diff --git a/go/examples/demo/main.go b/go/examples/demo/main.go index 9496656..21afb8f 100644 --- a/go/examples/demo/main.go +++ b/go/examples/demo/main.go @@ -25,7 +25,7 @@ import ( "sync" "time" - modcdp "github.com/pirate/ModCDP/go/modcdp" + modcdp "github.com/browserbase/modcdp/go/modcdp" "golang.org/x/term" ) diff --git a/go/go.mod b/go/go.mod index 0c850b8..ad58ae9 100644 --- a/go/go.mod +++ b/go/go.mod @@ -1,4 +1,4 @@ -module github.com/pirate/ModCDP/go +module github.com/browserbase/modcdp/go go 1.25.0 diff --git a/go/modcdp/client/ModCDPClient.go b/go/modcdp/client/ModCDPClient.go index 18c5c77..ed0346e 100644 --- a/go/modcdp/client/ModCDPClient.go +++ b/go/modcdp/client/ModCDPClient.go @@ -31,19 +31,19 @@ import ( "time" abxjsonschema "github.com/ArchiveBox/abxbus/abxbus-go/jsonschema" - "github.com/pirate/ModCDP/go/modcdp/injector" - "github.com/pirate/ModCDP/go/modcdp/launcher" - "github.com/pirate/ModCDP/go/modcdp/router" - "github.com/pirate/ModCDP/go/modcdp/translate" - transportpkg "github.com/pirate/ModCDP/go/modcdp/transport" - "github.com/pirate/ModCDP/go/modcdp/types" + "github.com/browserbase/modcdp/go/modcdp/injector" + "github.com/browserbase/modcdp/go/modcdp/launcher" + "github.com/browserbase/modcdp/go/modcdp/router" + "github.com/browserbase/modcdp/go/modcdp/translate" + transportpkg "github.com/browserbase/modcdp/go/modcdp/transport" + "github.com/browserbase/modcdp/go/modcdp/types" ) var ( extIDFromURL = regexp.MustCompile(`^chrome-extension://([a-z]+)/`) ) -const modcdpReadyExpression = `Boolean(globalThis.ModCDP?.__ModCDPServerVersion === 1 && globalThis.ModCDP?.handleCommand && globalThis.ModCDP?.addCustomEvent)` +const modcdpReadyExpression = `Boolean(globalThis.ModCDP?.__ModCDPServerVersion >= 1 && globalThis.ModCDP?.handleCommand && globalThis.ModCDP?.addCustomEvent)` const DefaultCDPSendTimeoutMS = 10_000 const DefaultEventWaitTimeoutMS = 10_000 @@ -1385,14 +1385,14 @@ func handlerPointer(handler Handler) uintptr { } func (c *ModCDPClient) Close() { - if c.transport != nil { - _ = c.transport.Close() - c.transport = nil - } if c.launchedBrowser != nil { c.launchedBrowser.Close() c.launchedBrowser = nil } + if c.transport != nil { + _ = c.transport.Close() + c.transport = nil + } for _, injector := range c.extensionInjectors { _ = injector.Close() } diff --git a/go/modcdp/injector/BBBrowserExtensionInjector_test.go b/go/modcdp/injector/BBBrowserExtensionInjector_test.go index 8e29ded..2f29526 100644 --- a/go/modcdp/injector/BBBrowserExtensionInjector_test.go +++ b/go/modcdp/injector/BBBrowserExtensionInjector_test.go @@ -2,7 +2,7 @@ package injector_test import ( "fmt" - modcdp "github.com/pirate/ModCDP/go/modcdp/client" + modcdp "github.com/browserbase/modcdp/go/modcdp/client" "os" "path/filepath" "regexp" diff --git a/go/modcdp/injector/BorrowedExtensionInjector.go b/go/modcdp/injector/BorrowedExtensionInjector.go index d38ed48..e82a852 100644 --- a/go/modcdp/injector/BorrowedExtensionInjector.go +++ b/go/modcdp/injector/BorrowedExtensionInjector.go @@ -153,7 +153,7 @@ const __name = (fn) => fn; %s const ModCDP = installModCDPServer(globalThis); return { - ok: Boolean(ModCDP?.__ModCDPServerVersion === 1 && ModCDP?.handleCommand && ModCDP?.addCustomEvent), + ok: Boolean(ModCDP?.__ModCDPServerVersion >= 1 && ModCDP?.handleCommand && ModCDP?.addCustomEvent), extension_id: globalThis.chrome?.runtime?.id ?? null, has_tabs: Boolean(globalThis.chrome?.tabs?.query), has_debugger: Boolean(globalThis.chrome?.debugger?.sendCommand && globalThis.chrome?.debugger?.getTargets), diff --git a/go/modcdp/injector/BorrowedExtensionInjector_test.go b/go/modcdp/injector/BorrowedExtensionInjector_test.go index 3f147c0..0dc5689 100644 --- a/go/modcdp/injector/BorrowedExtensionInjector_test.go +++ b/go/modcdp/injector/BorrowedExtensionInjector_test.go @@ -1,8 +1,8 @@ package injector_test import ( - modcdp "github.com/pirate/ModCDP/go/modcdp/client" - . "github.com/pirate/ModCDP/go/modcdp/injector" + modcdp "github.com/browserbase/modcdp/go/modcdp/client" + . "github.com/browserbase/modcdp/go/modcdp/injector" "path/filepath" "testing" ) diff --git a/go/modcdp/injector/DiscoveredExtensionInjector_test.go b/go/modcdp/injector/DiscoveredExtensionInjector_test.go index fc960e1..bc3db0b 100644 --- a/go/modcdp/injector/DiscoveredExtensionInjector_test.go +++ b/go/modcdp/injector/DiscoveredExtensionInjector_test.go @@ -2,8 +2,8 @@ package injector_test import ( "encoding/json" - modcdp "github.com/pirate/ModCDP/go/modcdp/client" - . "github.com/pirate/ModCDP/go/modcdp/injector" + modcdp "github.com/browserbase/modcdp/go/modcdp/client" + . "github.com/browserbase/modcdp/go/modcdp/injector" "os" "path/filepath" "testing" diff --git a/go/modcdp/injector/ExtensionInjector.go b/go/modcdp/injector/ExtensionInjector.go index b641203..40e08e3 100644 --- a/go/modcdp/injector/ExtensionInjector.go +++ b/go/modcdp/injector/ExtensionInjector.go @@ -6,7 +6,7 @@ import ( "strings" "time" - "github.com/pirate/ModCDP/go/modcdp/types" + "github.com/browserbase/modcdp/go/modcdp/types" ) const DefaultModCDPExtensionID = "mdedooklbnfejodmnhmkdpkaedafkehf" @@ -21,7 +21,7 @@ const DefaultTargetSessionPollIntervalMS = 20 var DefaultModCDPServiceWorkerURLSuffixes = []string{"/modcdp/service_worker.js"} var extIDFromURL = regexp.MustCompile(`^chrome-extension://([a-z]+)/`) -const modcdpReadyExpression = `Boolean(globalThis.ModCDP?.__ModCDPServerVersion === 1 && globalThis.ModCDP?.handleCommand && globalThis.ModCDP?.addCustomEvent)` +const modcdpReadyExpression = `Boolean(globalThis.ModCDP?.__ModCDPServerVersion >= 1 && globalThis.ModCDP?.handleCommand && globalThis.ModCDP?.addCustomEvent)` type SendCDP = types.SendCDP type SessionIDForTarget = types.SessionIDForTarget diff --git a/go/modcdp/injector/ExtensionInjector_test.go b/go/modcdp/injector/ExtensionInjector_test.go index c4157ff..416c77e 100644 --- a/go/modcdp/injector/ExtensionInjector_test.go +++ b/go/modcdp/injector/ExtensionInjector_test.go @@ -4,8 +4,8 @@ import ( "context" "encoding/json" "fmt" - modcdp "github.com/pirate/ModCDP/go/modcdp/client" - . "github.com/pirate/ModCDP/go/modcdp/injector" + modcdp "github.com/browserbase/modcdp/go/modcdp/client" + . "github.com/browserbase/modcdp/go/modcdp/injector" "path/filepath" "strings" "testing" @@ -256,7 +256,7 @@ func TestExtensionInjectorKeepsModCDPServiceWorkerAliveThroughOffscreenKeepalive t.Fatal(err) } versionResult, _ := version["result"].(map[string]any) - if versionResult["value"] != float64(1) { + if versionResult["value"] != float64(2) { t.Fatalf("ModCDP server version = %#v", versionResult["value"]) } } diff --git a/go/modcdp/injector/ExtensionsLoadUnpackedInjector_test.go b/go/modcdp/injector/ExtensionsLoadUnpackedInjector_test.go index 40fd8a1..3c89bad 100644 --- a/go/modcdp/injector/ExtensionsLoadUnpackedInjector_test.go +++ b/go/modcdp/injector/ExtensionsLoadUnpackedInjector_test.go @@ -4,8 +4,8 @@ import ( "context" "encoding/json" "fmt" - modcdp "github.com/pirate/ModCDP/go/modcdp/client" - . "github.com/pirate/ModCDP/go/modcdp/injector" + modcdp "github.com/browserbase/modcdp/go/modcdp/client" + . "github.com/browserbase/modcdp/go/modcdp/injector" "path/filepath" "strings" "testing" diff --git a/go/modcdp/injector/LocalBrowserLaunchExtensionInjector_test.go b/go/modcdp/injector/LocalBrowserLaunchExtensionInjector_test.go index 9cfe99c..60ea660 100644 --- a/go/modcdp/injector/LocalBrowserLaunchExtensionInjector_test.go +++ b/go/modcdp/injector/LocalBrowserLaunchExtensionInjector_test.go @@ -2,8 +2,8 @@ package injector_test import ( "archive/zip" - modcdp "github.com/pirate/ModCDP/go/modcdp/client" - . "github.com/pirate/ModCDP/go/modcdp/injector" + modcdp "github.com/browserbase/modcdp/go/modcdp/client" + . "github.com/browserbase/modcdp/go/modcdp/injector" "os" "path/filepath" "runtime" diff --git a/go/modcdp/injector/extension.zip b/go/modcdp/injector/extension.zip index e88eba8..268e970 100644 Binary files a/go/modcdp/injector/extension.zip and b/go/modcdp/injector/extension.zip differ diff --git a/go/modcdp/launcher/BrowserLauncher.go b/go/modcdp/launcher/BrowserLauncher.go index a21fad4..b4944d2 100644 --- a/go/modcdp/launcher/BrowserLauncher.go +++ b/go/modcdp/launcher/BrowserLauncher.go @@ -11,7 +11,7 @@ import ( "strings" "time" - "github.com/pirate/ModCDP/go/modcdp/types" + "github.com/browserbase/modcdp/go/modcdp/types" ) type LaunchOptions = types.LaunchOptions diff --git a/go/modcdp/modcdp.go b/go/modcdp/modcdp.go index 84cad6b..8e8cfd0 100644 --- a/go/modcdp/modcdp.go +++ b/go/modcdp/modcdp.go @@ -1,10 +1,10 @@ package modcdp import ( - "github.com/pirate/ModCDP/go/modcdp/client" - "github.com/pirate/ModCDP/go/modcdp/injector" - "github.com/pirate/ModCDP/go/modcdp/launcher" - "github.com/pirate/ModCDP/go/modcdp/transport" + "github.com/browserbase/modcdp/go/modcdp/client" + "github.com/browserbase/modcdp/go/modcdp/injector" + "github.com/browserbase/modcdp/go/modcdp/launcher" + "github.com/browserbase/modcdp/go/modcdp/transport" ) type ModCDPClient = client.ModCDPClient @@ -17,6 +17,7 @@ type ServerConfig = client.ServerConfig type CustomCommand = client.CustomCommand type CustomEvent = client.CustomEvent type CustomMiddleware = client.CustomMiddleware +type CDPEvent = client.CDPEvent type LaunchOptions = launcher.LaunchOptions type LaunchedBrowser = launcher.LaunchedBrowser type BrowserLauncher = launcher.BrowserLauncher diff --git a/go/modcdp/router/AutoSessionRouter_test.go b/go/modcdp/router/AutoSessionRouter_test.go index 98cb9d5..e41fb9d 100644 --- a/go/modcdp/router/AutoSessionRouter_test.go +++ b/go/modcdp/router/AutoSessionRouter_test.go @@ -9,9 +9,9 @@ import ( "testing" "time" + "github.com/browserbase/modcdp/go/modcdp/launcher" "github.com/gobwas/ws" "github.com/gobwas/ws/wsutil" - "github.com/pirate/ModCDP/go/modcdp/launcher" ) func TestAutoSessionRouterRejectsPendingExecutionContextWaitersWhenSessionDetaches(t *testing.T) { diff --git a/go/modcdp/translate/translate.go b/go/modcdp/translate/translate.go index 743cd03..1f1d187 100644 --- a/go/modcdp/translate/translate.go +++ b/go/modcdp/translate/translate.go @@ -138,7 +138,7 @@ func wrapCustomCommand(method string, params map[string]any, sessionID string) m m, _ := json.Marshal(method) p, _ := json.Marshal(params) sid, _ := json.Marshal(sessionID) - return callFunctionParams(fmt.Sprintf(`async function() { return await globalThis.ModCDP.handleCommand(%s, %s, %s); }`, string(m), string(p), string(sid))) + return callFunctionParams(fmt.Sprintf(`async function() { return JSON.stringify(await globalThis.ModCDP.handleCommand(%s, %s, %s)); }`, string(m), string(p), string(sid))) } func wrapServiceWorkerCommand(method string, params map[string]any, sessionID string, targetSessionID string) []rawStep { @@ -165,6 +165,7 @@ func wrapServiceWorkerCommand(method string, params map[string]any, sessionID st } } runtimeParams := map[string]any{} + unwrap := "runtime" switch method { case "Mod.evaluate": runtimeParams = wrapModCDPEvaluate(params, targetSessionID) @@ -178,8 +179,9 @@ func wrapServiceWorkerCommand(method string, params map[string]any, sessionID st cdpSessionID = targetSessionID } runtimeParams = wrapCustomCommand(method, params, cdpSessionID) + unwrap = "runtime_json" } - return []rawStep{{Method: "Runtime.callFunctionOn", Params: runtimeParams, Unwrap: "runtime"}} + return []rawStep{{Method: "Runtime.callFunctionOn", Params: runtimeParams, Unwrap: unwrap}} } func WrapCommandIfNeeded(method string, params map[string]any, routes map[string]string, sessionID string, targetSessionID ...string) (rawCommand, error) { @@ -198,7 +200,7 @@ func WrapCommandIfNeeded(method string, params map[string]any, routes map[string } func UnwrapResponseIfNeeded(result map[string]any, unwrap string) (any, error) { - if unwrap != "runtime" { + if unwrap != "runtime" && unwrap != "runtime_json" { return result, nil } if ex, ok := result["exceptionDetails"].(map[string]any); ok { @@ -219,7 +221,17 @@ func UnwrapResponseIfNeeded(result map[string]any, unwrap string) (any, error) { return nil, fmt.Errorf("%s", msg) } inner, _ := result["result"].(map[string]any) - return inner["value"], nil + value := inner["value"] + if unwrap == "runtime_json" { + if raw, ok := value.(string); ok { + var decoded any + if err := json.Unmarshal([]byte(raw), &decoded); err != nil { + return nil, err + } + return decoded, nil + } + } + return value, nil } func UnwrapEventIfNeeded(method string, params map[string]any, sessionID string, ourSessionID string) (string, any, bool) { diff --git a/go/modcdp/translate/translate_test.go b/go/modcdp/translate/translate_test.go index 60e3217..768347d 100644 --- a/go/modcdp/translate/translate_test.go +++ b/go/modcdp/translate/translate_test.go @@ -44,6 +44,14 @@ func TestTranslateRoutesWrapsAndUnwrapsModCDPProtocolMessagesDeterministically(t t.Fatalf("unwrap = %q", wrapped.Steps[0].Unwrap) } + configured, err := wrapCommandIfNeeded("Mod.configure", map[string]any{"server": map[string]any{"server_routes": map[string]any{"*.*": "loopback_cdp"}}}, DefaultClientRoutes(), "session-1") + if err != nil { + t.Fatal(err) + } + if configured.Steps[0].Unwrap != "runtime_json" { + t.Fatalf("configure unwrap = %q", configured.Steps[0].Unwrap) + } + 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 664cefb..0d0cc37 100644 --- a/go/modcdp/transport/NativeMessagingUpstreamTransport_test.go +++ b/go/modcdp/transport/NativeMessagingUpstreamTransport_test.go @@ -3,8 +3,8 @@ package transport_test import ( "encoding/json" "fmt" - modcdp "github.com/pirate/ModCDP/go/modcdp/client" - . "github.com/pirate/ModCDP/go/modcdp/transport" + modcdp "github.com/browserbase/modcdp/go/modcdp/client" + . "github.com/browserbase/modcdp/go/modcdp/transport" "net" "os" "path/filepath" diff --git a/go/modcdp/transport/NatsUpstreamTransport_test.go b/go/modcdp/transport/NatsUpstreamTransport_test.go index 52e08fd..4a1bc50 100644 --- a/go/modcdp/transport/NatsUpstreamTransport_test.go +++ b/go/modcdp/transport/NatsUpstreamTransport_test.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - . "github.com/pirate/ModCDP/go/modcdp/transport" + . "github.com/browserbase/modcdp/go/modcdp/transport" "os" "os/exec" "path/filepath" diff --git a/go/modcdp/transport/PipeUpstreamTransport.go b/go/modcdp/transport/PipeUpstreamTransport.go index 6e78d33..18c0225 100644 --- a/go/modcdp/transport/PipeUpstreamTransport.go +++ b/go/modcdp/transport/PipeUpstreamTransport.go @@ -5,7 +5,7 @@ import ( "os" "sync" - "github.com/pirate/ModCDP/go/modcdp/launcher" + "github.com/browserbase/modcdp/go/modcdp/launcher" ) type PipeUpstreamTransport struct { diff --git a/go/modcdp/transport/PipeUpstreamTransport_test.go b/go/modcdp/transport/PipeUpstreamTransport_test.go index 49c9bf5..6332e47 100644 --- a/go/modcdp/transport/PipeUpstreamTransport_test.go +++ b/go/modcdp/transport/PipeUpstreamTransport_test.go @@ -1,8 +1,8 @@ package transport_test import ( - modcdp "github.com/pirate/ModCDP/go/modcdp/client" - . "github.com/pirate/ModCDP/go/modcdp/transport" + modcdp "github.com/browserbase/modcdp/go/modcdp/client" + . "github.com/browserbase/modcdp/go/modcdp/transport" "os" "regexp" "runtime" diff --git a/go/modcdp/transport/ReverseWebSocketUpstreamTransport_test.go b/go/modcdp/transport/ReverseWebSocketUpstreamTransport_test.go index 8b82788..cc6de18 100644 --- a/go/modcdp/transport/ReverseWebSocketUpstreamTransport_test.go +++ b/go/modcdp/transport/ReverseWebSocketUpstreamTransport_test.go @@ -4,8 +4,8 @@ import ( "context" "encoding/json" "fmt" - modcdp "github.com/pirate/ModCDP/go/modcdp/client" - . "github.com/pirate/ModCDP/go/modcdp/transport" + modcdp "github.com/browserbase/modcdp/go/modcdp/client" + . "github.com/browserbase/modcdp/go/modcdp/transport" "os" "runtime" "strings" diff --git a/go/modcdp/transport/UpstreamTransport.go b/go/modcdp/transport/UpstreamTransport.go index c42b0b9..6247ab1 100644 --- a/go/modcdp/transport/UpstreamTransport.go +++ b/go/modcdp/transport/UpstreamTransport.go @@ -5,9 +5,9 @@ import ( "net" "sync" - "github.com/pirate/ModCDP/go/modcdp/injector" - "github.com/pirate/ModCDP/go/modcdp/launcher" - "github.com/pirate/ModCDP/go/modcdp/types" + "github.com/browserbase/modcdp/go/modcdp/injector" + "github.com/browserbase/modcdp/go/modcdp/launcher" + "github.com/browserbase/modcdp/go/modcdp/types" ) type ExtensionInjectorConfig = types.ExtensionInjectorConfig diff --git a/go/modcdp/transport/UpstreamTransport_test.go b/go/modcdp/transport/UpstreamTransport_test.go index ea61142..c6d5915 100644 --- a/go/modcdp/transport/UpstreamTransport_test.go +++ b/go/modcdp/transport/UpstreamTransport_test.go @@ -1,7 +1,7 @@ package transport_test import ( - . "github.com/pirate/ModCDP/go/modcdp/transport" + . "github.com/browserbase/modcdp/go/modcdp/transport" "strings" "testing" ) diff --git a/go/modcdp/transport/WebSocketUpstreamTransport_test.go b/go/modcdp/transport/WebSocketUpstreamTransport_test.go index 4ac468b..b93e5e8 100644 --- a/go/modcdp/transport/WebSocketUpstreamTransport_test.go +++ b/go/modcdp/transport/WebSocketUpstreamTransport_test.go @@ -1,8 +1,8 @@ package transport_test import ( - modcdp "github.com/pirate/ModCDP/go/modcdp/client" - . "github.com/pirate/ModCDP/go/modcdp/transport" + modcdp "github.com/browserbase/modcdp/go/modcdp/client" + . "github.com/browserbase/modcdp/go/modcdp/transport" "net/url" "strings" "testing" diff --git a/js/src/client/ModCDPClient.ts b/js/src/client/ModCDPClient.ts index 0f3a0e9..2e136c7 100644 --- a/js/src/client/ModCDPClient.ts +++ b/js/src/client/ModCDPClient.ts @@ -28,35 +28,8 @@ import { type UpstreamMode, type UpstreamTransport, } from "../transport/UpstreamTransport.js"; -import { - DEFAULT_UPSTREAM_REVERSEWS_BIND, - DEFAULT_UPSTREAM_REVERSEWS_WAIT_TIMEOUT_MS, - ReverseWebSocketUpstreamTransport, -} from "../transport/ReverseWebSocketUpstreamTransport.js"; -import { WebSocketUpstreamTransport } from "../transport/WebSocketUpstreamTransport.js"; -import { - DEFAULT_UPSTREAM_NATIVEMESSAGING_WAIT_TIMEOUT_MS, - NativeMessagingUpstreamTransport, -} from "../transport/NativeMessagingUpstreamTransport.js"; -import { PipeUpstreamTransport } from "../transport/PipeUpstreamTransport.js"; -import { DEFAULT_UPSTREAM_NATS_WAIT_TIMEOUT_MS, NatsUpstreamTransport } from "../transport/NatsUpstreamTransport.js"; -import { LocalBrowserLauncher } from "../launcher/LocalBrowserLauncher.js"; -import { RemoteBrowserLauncher } from "../launcher/RemoteBrowserLauncher.js"; -import { BrowserbaseBrowserLauncher } from "../launcher/BrowserbaseBrowserLauncher.js"; -import { NoopBrowserLauncher } from "../launcher/NoopBrowserLauncher.js"; import type { BrowserLauncher, BrowserLaunchOptions, LaunchedBrowser } from "../launcher/BrowserLauncher.js"; -import { BBBrowserExtensionInjector } from "../injector/BBBrowserExtensionInjector.js"; -import { BorrowedExtensionInjector } from "../injector/BorrowedExtensionInjector.js"; -import { DiscoveredExtensionInjector } from "../injector/DiscoveredExtensionInjector.js"; -import { - DEFAULT_MODCDP_SERVICE_WORKER_URL_SUFFIXES, - DEFAULT_MODCDP_WAKE_PATH, - ExtensionInjector, - type ExtensionInjectorConfig, - type SendCDP, -} from "../injector/ExtensionInjector.js"; -import { ExtensionsLoadUnpackedInjector } from "../injector/ExtensionsLoadUnpackedInjector.js"; -import { LocalBrowserLaunchExtensionInjector } from "../injector/LocalBrowserLaunchExtensionInjector.js"; +import { type ExtensionInjectorConfig, type ExtensionInjector, type SendCDP } from "../injector/ExtensionInjector.js"; import { AutoSessionRouter } from "../router/AutoSessionRouter.js"; import type { CdpCommandMessage, @@ -95,6 +68,12 @@ export const DEFAULT_SERVICE_WORKER_READY_TIMEOUT_MS = 60_000; 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; +export const DEFAULT_UPSTREAM_NATS_WAIT_TIMEOUT_MS = 10_000; type PendingCommand = { method: string; @@ -770,9 +749,9 @@ export class ModCDPClient extends ModCDPEventEmitter { async _connectUpstreamTransport() { if (this.transport) return; - const launcher = this._browserLauncher(); - const transport = this._upstreamTransport(); - const injectors = this._injectorsForConfig(); + const launcher = await this._browserLauncher(); + const transport = await this._upstreamTransport(); + const injectors = await this._injectorsForConfig(); this._injectors = injectors; const initial_transport_config = this._upstreamTransportConfig(); transport.update(initial_transport_config); @@ -821,29 +800,60 @@ export class ModCDPClient extends ModCDPEventEmitter { } } - _upstreamTransport(): UpstreamTransport { - const factories = { - ws: () => new WebSocketUpstreamTransport(), - pipe: () => new PipeUpstreamTransport(), - reversews: () => new ReverseWebSocketUpstreamTransport(), - nativemessaging: () => new NativeMessagingUpstreamTransport(), - nats: () => new NatsUpstreamTransport(), - } satisfies Record UpstreamTransport>; - const factory = factories[this.upstream.upstream_mode as UpstreamMode]; - if (!factory) throw new Error(`unknown upstream.upstream_mode=${this.upstream.upstream_mode}`); - return factory(); + async _upstreamTransport(): Promise { + switch (this.upstream.upstream_mode as UpstreamMode) { + case "ws": { + const { WebSocketUpstreamTransport } = await import("../transport/WebSocketUpstreamTransport.js"); + return new WebSocketUpstreamTransport(); + } + case "pipe": { + const { PipeUpstreamTransport } = await import(/* @vite-ignore */ "../transport/PipeUpstreamTransport.js"); + return new PipeUpstreamTransport(); + } + case "reversews": { + const { ReverseWebSocketUpstreamTransport } = await import( + /* @vite-ignore */ "../transport/ReverseWebSocketUpstreamTransport.js" + ); + return new ReverseWebSocketUpstreamTransport(); + } + case "nativemessaging": { + const { NativeMessagingUpstreamTransport } = await import( + /* @vite-ignore */ "../transport/NativeMessagingUpstreamTransport.js" + ); + return new NativeMessagingUpstreamTransport(); + } + case "nats": { + const { NatsUpstreamTransport } = await import(/* @vite-ignore */ "../transport/NatsUpstreamTransport.js"); + return new NatsUpstreamTransport(); + } + default: + throw new Error(`unknown upstream.upstream_mode=${this.upstream.upstream_mode}`); + } } - _browserLauncher(): BrowserLauncher { - const factories = { - local: () => new LocalBrowserLauncher(this.launcher.launcher_options), - remote: () => new RemoteBrowserLauncher(this.launcher.launcher_options, this.upstream.upstream_cdp_url), - bb: () => new BrowserbaseBrowserLauncher(this.launcher.launcher_options), - none: () => new NoopBrowserLauncher(this.launcher.launcher_options), - } satisfies Record BrowserLauncher>; - const factory = factories[this.launcher.launcher_mode as LauncherMode]; - if (!factory) throw new Error(`unknown launcher.launcher_mode=${this.launcher.launcher_mode}`); - return factory(); + async _browserLauncher(): Promise { + switch (this.launcher.launcher_mode as LauncherMode) { + case "local": { + const { LocalBrowserLauncher } = await import(/* @vite-ignore */ "../launcher/LocalBrowserLauncher.js"); + return new LocalBrowserLauncher(this.launcher.launcher_options); + } + case "remote": { + const { RemoteBrowserLauncher } = await import("../launcher/RemoteBrowserLauncher.js"); + return new RemoteBrowserLauncher(this.launcher.launcher_options, this.upstream.upstream_cdp_url); + } + case "bb": { + const { BrowserbaseBrowserLauncher } = await import( + /* @vite-ignore */ "../launcher/BrowserbaseBrowserLauncher.js" + ); + return new BrowserbaseBrowserLauncher(this.launcher.launcher_options); + } + case "none": { + const { NoopBrowserLauncher } = await import("../launcher/NoopBrowserLauncher.js"); + return new NoopBrowserLauncher(this.launcher.launcher_options); + } + default: + throw new Error(`unknown launcher.launcher_mode=${this.launcher.launcher_mode}`); + } } _launcherOptions(): BrowserLaunchOptions { @@ -875,18 +885,33 @@ export class ModCDPClient extends ModCDPEventEmitter { }; } - _injectorsForConfig() { + async _injectorsForConfig() { if (this.injector.injector_mode === "none") return []; const injectors: ExtensionInjector[] = []; if (this.injector.injector_mode === "auto" || this.injector.injector_mode === "discover") { + const { DiscoveredExtensionInjector } = await import("../injector/DiscoveredExtensionInjector.js"); injectors.push(new DiscoveredExtensionInjector()); } if (this.injector.injector_mode === "auto" || this.injector.injector_mode === "inject") { - if (this.launcher.launcher_mode === "bb") injectors.push(new BBBrowserExtensionInjector()); - if (this.launcher.launcher_mode === "local") injectors.push(new LocalBrowserLaunchExtensionInjector()); + if (this.launcher.launcher_mode === "bb") { + const { BBBrowserExtensionInjector } = await import( + /* @vite-ignore */ "../injector/BBBrowserExtensionInjector.js" + ); + injectors.push(new BBBrowserExtensionInjector()); + } + if (this.launcher.launcher_mode === "local") { + const { LocalBrowserLaunchExtensionInjector } = await import( + /* @vite-ignore */ "../injector/LocalBrowserLaunchExtensionInjector.js" + ); + injectors.push(new LocalBrowserLaunchExtensionInjector()); + } + const { ExtensionsLoadUnpackedInjector } = await import( + /* @vite-ignore */ "../injector/ExtensionsLoadUnpackedInjector.js" + ); injectors.push(new ExtensionsLoadUnpackedInjector()); } if (this.injector.injector_mode === "auto" || this.injector.injector_mode === "borrow") { + const { BorrowedExtensionInjector } = await import(/* @vite-ignore */ "../injector/BorrowedExtensionInjector.js"); injectors.push(new BorrowedExtensionInjector()); } if (injectors.length === 0) throw new Error(`unknown injector.injector_mode=${this.injector.injector_mode}`); @@ -925,7 +950,7 @@ export class ModCDPClient extends ModCDPEventEmitter { } async _runInjectors(send: SendCDP, injectors: ExtensionInjector[] | null = null) { - injectors ??= this._injectorsForConfig(); + injectors ??= await this._injectorsForConfig(); const errors: string[] = []; for (const injector of injectors) { injector.update(this._baseInjectorConfig(send)); @@ -959,10 +984,10 @@ export class ModCDPClient extends ModCDPEventEmitter { async close() { for (const cleanup of this.event_wait_cleanups) cleanup(); this.event_wait_cleanups.clear(); - await this.transport?.close(); - this.transport = null; if (this._launched) await this._launched.close(); this._launched = null; + await this.transport?.close(); + this.transport = null; for (const injector of this._injectors) await injector.close(); this._injectors = []; } diff --git a/js/src/injector/BBBrowserExtensionInjector.ts b/js/src/injector/BBBrowserExtensionInjector.ts index 299ae93..b523813 100644 --- a/js/src/injector/BBBrowserExtensionInjector.ts +++ b/js/src/injector/BBBrowserExtensionInjector.ts @@ -85,7 +85,12 @@ export class BBBrowserExtensionInjector extends ExtensionInjector { const fs = await import("node:fs"); const path = await import("node:path"); const form = new FormData(); - form.append("file", new Blob([fs.readFileSync(zip_path)]), path.basename(zip_path)); + const zip_bytes = fs.readFileSync(zip_path); + const zip_array_buffer = zip_bytes.buffer.slice( + zip_bytes.byteOffset, + zip_bytes.byteOffset + zip_bytes.byteLength, + ) as ArrayBuffer; + form.append("file", new Blob([zip_array_buffer]), path.basename(zip_path)); const response = await fetch(new URL("/v1/extensions", `${base_url.replace(/\/$/, "")}/`), { method: "POST", headers: { "X-BB-API-Key": browserbase_api_key }, diff --git a/js/src/injector/BorrowedExtensionInjector.ts b/js/src/injector/BorrowedExtensionInjector.ts index 2b208ea..48f5cca 100644 --- a/js/src/injector/BorrowedExtensionInjector.ts +++ b/js/src/injector/BorrowedExtensionInjector.ts @@ -4,14 +4,14 @@ import { ExtensionInjector, type ExtensionInjectionResult, type TargetInfo } fro const EXT_ID_FROM_URL = /^chrome-extension:\/\/([a-z]+)\//; const MODCDP_READY_EXPRESSION = - "Boolean(globalThis.ModCDP?.__ModCDPServerVersion === 1 && globalThis.ModCDP?.handleCommand && globalThis.ModCDP?.addCustomEvent)"; + "Boolean(globalThis.ModCDP?.__ModCDPServerVersion >= 1 && globalThis.ModCDP?.handleCommand && globalThis.ModCDP?.addCustomEvent)"; const bootstrap_modcdp_server_expression = ` function() { const __name = (fn) => fn; const installModCDPServer = ${installModCDPServer.toString()}; const ModCDP = installModCDPServer(globalThis); return { - ok: Boolean(ModCDP?.__ModCDPServerVersion === 1 && ModCDP?.handleCommand && ModCDP?.addCustomEvent), + ok: Boolean(ModCDP?.__ModCDPServerVersion >= 1 && ModCDP?.handleCommand && ModCDP?.addCustomEvent), extension_id: globalThis.chrome?.runtime?.id ?? null, has_tabs: Boolean(globalThis.chrome?.tabs?.query), has_debugger: Boolean(globalThis.chrome?.debugger?.sendCommand && globalThis.chrome?.debugger?.getTargets), diff --git a/js/src/injector/ExtensionInjector.ts b/js/src/injector/ExtensionInjector.ts index 56ba1ac..3d1719d 100644 --- a/js/src/injector/ExtensionInjector.ts +++ b/js/src/injector/ExtensionInjector.ts @@ -3,14 +3,13 @@ import type { UpstreamTransportConfig } from "../transport/UpstreamTransport.js" import type { ProtocolParams, ProtocolResult } from "../types/modcdp.js"; import { commands as RuntimeCommands } from "../types/generated/zod/Runtime.js"; import { commands as TargetCommands } from "../types/generated/zod/Target.js"; -import { existsSync } from "node:fs"; 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)"; + "Boolean(globalThis.ModCDP?.__ModCDPServerVersion >= 1 && globalThis.ModCDP?.handleCommand && globalThis.ModCDP?.addCustomEvent)"; export const DEFAULT_CDP_SEND_TIMEOUT_MS = 10_000; export const DEFAULT_EXECUTION_CONTEXT_TIMEOUT_MS = 10_000; export const DEFAULT_SERVICE_WORKER_PROBE_TIMEOUT_MS = 10_000; @@ -65,10 +64,10 @@ function delay(ms: number) { export function defaultModCDPExtensionPath() { if (typeof process === "object" && process?.versions?.node && import.meta.url.startsWith("file:")) { - const candidates = ["../../../dist/extension.zip", "../../../../dist/extension.zip"].map((relative_path) => - decodeURIComponent(new URL(/* @vite-ignore */ relative_path, import.meta.url).pathname), - ); - return candidates.find((candidate) => existsSync(candidate)) ?? candidates[0]; + const relative_path = import.meta.url.includes("/dist/js/src/") + ? "../../../../dist/extension.zip" + : "../../../dist/extension.zip"; + return decodeURIComponent(new URL(/* @vite-ignore */ relative_path, import.meta.url).pathname); } return "../../../dist/extension.zip"; } diff --git a/js/src/proxy/ProxyConnectionState.ts b/js/src/proxy/ProxyConnectionState.ts index 4226586..7857f06 100644 --- a/js/src/proxy/ProxyConnectionState.ts +++ b/js/src/proxy/ProxyConnectionState.ts @@ -8,6 +8,7 @@ export const ProxyPendingSchema = z client_id: z.number().optional(), client_session_id: z.string().nullable().optional(), event_name: z.string().optional(), + unwrap: z.enum(["runtime", "runtime_json"]).optional(), resolve: z.custom<(value: ProtocolResult) => void>().optional(), reject: z.custom<(error: Error) => void>().optional(), }) diff --git a/js/src/proxy/proxy.ts b/js/src/proxy/proxy.ts index 6f6b6a4..3065ae1 100644 --- a/js/src/proxy/proxy.ts +++ b/js/src/proxy/proxy.ts @@ -930,12 +930,8 @@ function handleClientMessage(state: ProxyConnectionState, buf: RawData) { // so the response can be steered back to the right Playwright CDPSession. if (MAGIC_METHODS.has(method) || ROUTE_TO_SW_RE.test(method)) { const upId = state.next_upstream_id++; - state.pending.set(upId, { - kind: "modcdp_eval", - client_id: id, - client_session_id: sessionId || null, - }); let runtimeParams; + let unwrap: "runtime" | "runtime_json" = "runtime"; if (method === "Mod.evaluate") { const evaluateParams = ModCDPEvaluateParamsSchema.parse(params ?? {}); runtimeParams = wrapModCDPEvaluate({ @@ -957,7 +953,14 @@ function handleClientMessage(state: ProxyConnectionState, buf: RawData) { ? params.cdpSessionId : (sessionId ?? null); runtimeParams = wrapCustomCommand(method, params, cdpSessionId); + unwrap = "runtime_json"; } + state.pending.set(upId, { + kind: "modcdp_eval", + client_id: id, + client_session_id: sessionId || null, + unwrap, + }); if (state.ext_execution_context_id != null) runtimeParams.executionContextId = state.ext_execution_context_id; state.upstream.send( JSON.stringify({ @@ -1008,7 +1011,7 @@ function handleUpstreamMessage(state: ProxyConnectionState, msg: CdpResponseMess result: unwrapResponseIfNeeded( (response.result === undefined ? {} : response.result) as ProtocolResult, - "runtime", + p.unwrap ?? "runtime", ) ?? {}, }); } catch (e) { diff --git a/js/src/server/ModCDPServer.ts b/js/src/server/ModCDPServer.ts index b5fde96..846bce3 100644 --- a/js/src/server/ModCDPServer.ts +++ b/js/src/server/ModCDPServer.ts @@ -42,7 +42,7 @@ type ModCDPGlobalScope = typeof globalThis & }; export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis as ModCDPGlobalScope) { - const MODCDP_SERVER_VERSION = 1; + const MODCDP_SERVER_VERSION = 2; const DEFAULT_CDP_SEND_TIMEOUT_MS = 10_000; const DEFAULT_LOOPBACK_EXECUTION_CONTEXT_TIMEOUT_MS = 10_000; const DEFAULT_WS_CONNECT_ERROR_SETTLE_TIMEOUT_MS = 250; @@ -638,7 +638,7 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis if (!chromeApi?.debugger?.getTargets || !chromeApi?.debugger?.attach) { throw new Error("chrome.debugger is unavailable for reverse expression evaluation."); } - const serviceWorkerUrl = chromeApi.runtime.getURL("modcdp/service_worker.js"); + const serviceWorkerUrl = currentServiceWorkerUrl(); const targets = await chromeApi.debugger.getTargets(); const target = targets.find((candidate) => candidate.url === serviceWorkerUrl); if (!target?.id) throw new Error(`Could not find ModCDP service worker debugger target ${serviceWorkerUrl}.`); @@ -654,6 +654,20 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis return debuggee; } + function currentServiceWorkerUrl() { + const chromeApi = globalScope.chrome; + const manifest = chromeApi?.runtime?.getManifest?.(); + const service_worker = + manifest && typeof manifest === "object" && "background" in manifest + ? (manifest.background as { service_worker?: unknown } | undefined)?.service_worker + : null; + const service_worker_path = + typeof service_worker === "string" && service_worker.length > 0 + ? service_worker.replace(/^\//, "") + : "modcdp/service_worker.js"; + return chromeApi.runtime.getURL(service_worker_path); + } + async function evaluateInSelf(expression: string): Promise { const debuggee = await getSelfDebuggee(); const result = (await debuggerSendCommand(debuggee, "Runtime.evaluate", { @@ -1064,7 +1078,12 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis })() `)) as Record; if (result?.__ModCDP_middleware_next__ === true && typeof next === "function") { - return await next(result.value); + const nextResult = await next(result.value); + const { __ModCDP_middleware_next__, value, ...overrides } = result; + if (Object.keys(overrides).length === 0) return nextResult; + return nextResult != null && typeof nextResult === "object" && !Array.isArray(nextResult) + ? { ...(nextResult as Record), ...overrides } + : overrides; } return result; }; @@ -1196,11 +1215,9 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis this.loopback_cdp_url = version.webSocketDebuggerUrl; const { targetInfos } = (await callLoopbackWS("Target.getTargets")) as cdp.types.ts.Target.GetTargetsResult; - const chromeApi = globalScope.chrome; + const serviceWorkerUrl = currentServiceWorkerUrl(); const worker = targetInfos.find( - (target) => - target.type === "service_worker" && - target.url === `chrome-extension://${chromeApi.runtime.id}/modcdp/service_worker.js`, + (target) => target.type === "service_worker" && target.url === serviceWorkerUrl, ); if (!worker) return fail(version); diff --git a/js/src/translate/translate.ts b/js/src/translate/translate.ts index c63ddc2..2338f2d 100644 --- a/js/src/translate/translate.ts +++ b/js/src/translate/translate.ts @@ -175,7 +175,7 @@ export function wrapCustomCommand( cdpSessionId: string | null = null, ): cdp.types.ts.Runtime.EvaluateParams { return { - functionDeclaration: `async function() { return await globalThis.ModCDP.handleCommand(${JSON.stringify(method)}, ${JSON.stringify(params)}, ${JSON.stringify(cdpSessionId)}); }`, + functionDeclaration: `async function() { return JSON.stringify(await globalThis.ModCDP.handleCommand(${JSON.stringify(method)}, ${JSON.stringify(params)}, ${JSON.stringify(cdpSessionId)})); }`, awaitPromise: true, returnByValue: true, }; @@ -199,6 +199,7 @@ function wrapServiceWorkerCommand(method: string, params: ProtocolParams = {}, c } let runtimeParams; + let unwrap: "runtime" | "runtime_json" = "runtime"; if (method === "Mod.evaluate") { const evaluateParams = params as ModCDPEvaluateParams; runtimeParams = wrapModCDPEvaluate({ @@ -215,13 +216,14 @@ function wrapServiceWorkerCommand(method: string, params: ProtocolParams = {}, c params, ((params as ModCDPCustomPayload).cdpSessionId as string) ?? cdpSessionId, ); + unwrap = "runtime_json"; } return [ { method: "Runtime.callFunctionOn", params: runtimeParams, - unwrap: "runtime" as const, + unwrap, }, ]; } @@ -260,10 +262,16 @@ function unwrapRuntimeResponse(result: cdp.types.ts.Runtime.EvaluateResult) { return result?.result?.value; } +function unwrapRuntimeJsonResponse(result: cdp.types.ts.Runtime.EvaluateResult) { + const value = unwrapRuntimeResponse(result); + return typeof value === "string" ? JSON.parse(value) : value; +} + export function unwrapResponseIfNeeded( result: ProtocolResult | cdp.types.ts.Runtime.EvaluateResult, unwrap: string | null = null, ) { + if (unwrap === "runtime_json") return unwrapRuntimeJsonResponse(result as cdp.types.ts.Runtime.EvaluateResult); return unwrap === "runtime" ? unwrapRuntimeResponse(result as cdp.types.ts.Runtime.EvaluateResult) : (result ?? {}); } diff --git a/js/src/types/modcdp.ts b/js/src/types/modcdp.ts index bb4d638..5cb398a 100644 --- a/js/src/types/modcdp.ts +++ b/js/src/types/modcdp.ts @@ -387,7 +387,7 @@ export const TranslatedStepSchema = z method: z.string(), params: ProtocolParamsSchema.optional(), sessionId: z.string().nullable().optional(), - unwrap: z.literal("runtime").optional(), + unwrap: z.enum(["runtime", "runtime_json"]).optional(), }) .passthrough(); export type TranslatedStep = z.infer; diff --git a/js/test/test.ExtensionInjector.ts b/js/test/test.ExtensionInjector.ts index 0918773..ad22de7 100644 --- a/js/test/test.ExtensionInjector.ts +++ b/js/test/test.ExtensionInjector.ts @@ -135,7 +135,7 @@ test("ExtensionInjector keeps the ModCDP service worker alive through offscreen { expression: "globalThis.ModCDP?.__ModCDPServerVersion", returnByValue: true }, session_id, ); - assert.equal((version.result as { value?: unknown }).value, 1); + assert.equal((version.result as { value?: unknown }).value, 2); } finally { await cdp.close(); await injector.close(); diff --git a/js/test/test.ModCDPClient.ts b/js/test/test.ModCDPClient.ts index 2e6988a..69f3752 100644 --- a/js/test/test.ModCDPClient.ts +++ b/js/test/test.ModCDPClient.ts @@ -293,19 +293,19 @@ test("ModCDPClient defaults launched ModCDP-server upstreams to extension auto", } }); -test("ModCDPClient rejects unknown component modes at their owning factory boundary", () => { - assert.throws( +test("ModCDPClient rejects unknown component modes at their owning factory boundary", async () => { + await assert.rejects( () => new ModCDPClient({ upstream: { upstream_mode: "bogus" as any }, })._upstreamTransport(), /unknown upstream\.upstream_mode=bogus/, ); - assert.throws( + await assert.rejects( () => new ModCDPClient({ launcher: { launcher_mode: "bogus" as any } })._browserLauncher(), /unknown launcher\.launcher_mode=bogus/, ); - assert.throws( + await assert.rejects( () => new ModCDPClient({ injector: { injector_mode: "bogus" as any } })._injectorsForConfig(), /unknown injector\.injector_mode=bogus/, ); diff --git a/js/test/test.ReverseWebSocketUpstreamTransport.ts b/js/test/test.ReverseWebSocketUpstreamTransport.ts index ef95a03..e3cdff5 100644 --- a/js/test/test.ReverseWebSocketUpstreamTransport.ts +++ b/js/test/test.ReverseWebSocketUpstreamTransport.ts @@ -135,7 +135,10 @@ test("reversews upstream accepts a real extension reverse connection and routes const reverse = new ModCDPClient({ launcher: { launcher_mode: "local", - launcher_options: { headless: true, sandbox: process.platform !== "linux" }, + launcher_options: { + headless: process.platform === "linux" && !process.env.DISPLAY, + sandbox: process.platform !== "linux", + }, }, upstream: { upstream_mode: "reversews", upstream_reversews_bind: reverse_bind }, injector: { diff --git a/js/test/test.translate.ts b/js/test/test.translate.ts index fcfbc1f..fa122de 100644 --- a/js/test/test.translate.ts +++ b/js/test/test.translate.ts @@ -28,6 +28,9 @@ test("translate routes, wraps, and unwraps ModCDP protocol messages deterministi assert.match(String(wrapped.steps[0]?.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"); + assert.deepEqual(unwrapResponseIfNeeded({ result: { type: "object", value: { ok: true } } }, "runtime"), { ok: true, }); diff --git a/package.json b/package.json index d479eda..e706aa5 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,11 @@ { "name": "modcdp", - "version": "0.0.6", + "version": "0.0.13", "repository": { "type": "git", "url": "git+https://github.com/pirate/modcdp.git" }, "files": [ - "js/src/**/*.ts", - "js/examples/**/*.ts", "js/scripts/**/*.mjs", "dist/**/*", "dist/extension.zip", @@ -20,106 +18,115 @@ "type": "module", "exports": { ".": { - "types": "./js/src/index.ts", + "types": "./dist/js/src/index.d.ts", "import": "./dist/js/src/index.js", "default": "./dist/js/src/index.js" }, "./client": { - "types": "./js/src/client/ModCDPClient.ts", + "types": "./dist/js/src/client/ModCDPClient.d.ts", "import": "./dist/js/src/client/ModCDPClient.js", "default": "./dist/js/src/client/ModCDPClient.js" }, "./client/ModCDPClient.js": { - "types": "./js/src/client/ModCDPClient.ts", + "types": "./dist/js/src/client/ModCDPClient.d.ts", "import": "./dist/js/src/client/ModCDPClient.js" }, "./server/ModCDPServer.js": { - "types": "./js/src/server/ModCDPServer.ts", + "types": "./dist/js/src/server/ModCDPServer.d.ts", "import": "./dist/js/src/server/ModCDPServer.js" }, "./launcher/BrowserLauncher.js": { - "types": "./js/src/launcher/BrowserLauncher.ts", + "types": "./dist/js/src/launcher/BrowserLauncher.d.ts", "import": "./dist/js/src/launcher/BrowserLauncher.js" }, "./launcher/LocalBrowserLauncher.js": { - "types": "./js/src/launcher/LocalBrowserLauncher.ts", + "types": "./dist/js/src/launcher/LocalBrowserLauncher.d.ts", "import": "./dist/js/src/launcher/LocalBrowserLauncher.js" }, "./launcher/RemoteBrowserLauncher.js": { - "types": "./js/src/launcher/RemoteBrowserLauncher.ts", + "types": "./dist/js/src/launcher/RemoteBrowserLauncher.d.ts", "import": "./dist/js/src/launcher/RemoteBrowserLauncher.js" }, "./launcher/BrowserbaseBrowserLauncher.js": { - "types": "./js/src/launcher/BrowserbaseBrowserLauncher.ts", + "types": "./dist/js/src/launcher/BrowserbaseBrowserLauncher.d.ts", "import": "./dist/js/src/launcher/BrowserbaseBrowserLauncher.js" }, "./launcher/NoopBrowserLauncher.js": { - "types": "./js/src/launcher/NoopBrowserLauncher.ts", + "types": "./dist/js/src/launcher/NoopBrowserLauncher.d.ts", "import": "./dist/js/src/launcher/NoopBrowserLauncher.js" }, "./transport/UpstreamTransport.js": { - "types": "./js/src/transport/UpstreamTransport.ts", + "types": "./dist/js/src/transport/UpstreamTransport.d.ts", "import": "./dist/js/src/transport/UpstreamTransport.js" }, "./transport/WebSocketUpstreamTransport.js": { - "types": "./js/src/transport/WebSocketUpstreamTransport.ts", + "types": "./dist/js/src/transport/WebSocketUpstreamTransport.d.ts", "import": "./dist/js/src/transport/WebSocketUpstreamTransport.js" }, "./transport/ReverseWebSocketUpstreamTransport.js": { - "types": "./js/src/transport/ReverseWebSocketUpstreamTransport.ts", + "types": "./dist/js/src/transport/ReverseWebSocketUpstreamTransport.d.ts", "import": "./dist/js/src/transport/ReverseWebSocketUpstreamTransport.js" }, "./transport/NativeMessagingUpstreamTransport.js": { - "types": "./js/src/transport/NativeMessagingUpstreamTransport.ts", + "types": "./dist/js/src/transport/NativeMessagingUpstreamTransport.d.ts", "import": "./dist/js/src/transport/NativeMessagingUpstreamTransport.js" }, "./transport/PipeUpstreamTransport.js": { - "types": "./js/src/transport/PipeUpstreamTransport.ts", + "types": "./dist/js/src/transport/PipeUpstreamTransport.d.ts", "import": "./dist/js/src/transport/PipeUpstreamTransport.js" }, "./transport/NatsUpstreamTransport.js": { - "types": "./js/src/transport/NatsUpstreamTransport.ts", + "types": "./dist/js/src/transport/NatsUpstreamTransport.d.ts", "import": "./dist/js/src/transport/NatsUpstreamTransport.js" }, "./injector/ExtensionInjector.js": { - "types": "./js/src/injector/ExtensionInjector.ts", + "types": "./dist/js/src/injector/ExtensionInjector.d.ts", "import": "./dist/js/src/injector/ExtensionInjector.js" }, "./injector/DiscoveredExtensionInjector.js": { - "types": "./js/src/injector/DiscoveredExtensionInjector.ts", + "types": "./dist/js/src/injector/DiscoveredExtensionInjector.d.ts", "import": "./dist/js/src/injector/DiscoveredExtensionInjector.js" }, "./injector/LocalBrowserLaunchExtensionInjector.js": { - "types": "./js/src/injector/LocalBrowserLaunchExtensionInjector.ts", + "types": "./dist/js/src/injector/LocalBrowserLaunchExtensionInjector.d.ts", "import": "./dist/js/src/injector/LocalBrowserLaunchExtensionInjector.js" }, "./injector/BBBrowserExtensionInjector.js": { - "types": "./js/src/injector/BBBrowserExtensionInjector.ts", + "types": "./dist/js/src/injector/BBBrowserExtensionInjector.d.ts", "import": "./dist/js/src/injector/BBBrowserExtensionInjector.js" }, "./injector/ExtensionsLoadUnpackedInjector.js": { - "types": "./js/src/injector/ExtensionsLoadUnpackedInjector.ts", + "types": "./dist/js/src/injector/ExtensionsLoadUnpackedInjector.d.ts", "import": "./dist/js/src/injector/ExtensionsLoadUnpackedInjector.js" }, "./injector/BorrowedExtensionInjector.js": { - "types": "./js/src/injector/BorrowedExtensionInjector.ts", + "types": "./dist/js/src/injector/BorrowedExtensionInjector.d.ts", "import": "./dist/js/src/injector/BorrowedExtensionInjector.js" }, "./router/AutoSessionRouter.js": { - "types": "./js/src/router/AutoSessionRouter.ts", + "types": "./dist/js/src/router/AutoSessionRouter.d.ts", "import": "./dist/js/src/router/AutoSessionRouter.js" }, "./proxy/proxy.js": { - "types": "./js/src/proxy/proxy.ts", + "types": "./dist/js/src/proxy/proxy.d.ts", "import": "./dist/js/src/proxy/proxy.js" }, "./proxy/cli.js": { - "types": "./js/src/proxy/cli.ts", + "types": "./dist/js/src/proxy/cli.d.ts", "import": "./dist/js/src/proxy/cli.js" }, - "./translate/translate.js": "./dist/js/src/translate/translate.js", - "./types/*": "./dist/js/src/types/*", - "./types/generated/*": "./dist/js/src/types/generated/*" + "./translate/translate.js": { + "types": "./dist/js/src/translate/translate.d.ts", + "import": "./dist/js/src/translate/translate.js" + }, + "./types/*": { + "types": "./dist/js/src/types/*.d.ts", + "import": "./dist/js/src/types/*.js" + }, + "./types/generated/*": { + "types": "./dist/js/src/types/generated/*.d.ts", + "import": "./dist/js/src/types/generated/*.js" + } }, "scripts": { "clean": "rm -rf dist", diff --git a/python/modcdp/client/ModCDPClient.py b/python/modcdp/client/ModCDPClient.py index 5b56dbd..6639219 100644 --- a/python/modcdp/client/ModCDPClient.py +++ b/python/modcdp/client/ModCDPClient.py @@ -164,7 +164,7 @@ def ping(self, **params: Any): return self._client._send_command("Mod.ping", params) MODCDP_READY_EXPRESSION = ( - "Boolean(globalThis.ModCDP?.__ModCDPServerVersion === 1 && " + "Boolean(globalThis.ModCDP?.__ModCDPServerVersion >= 1 && " "globalThis.ModCDP?.handleCommand && globalThis.ModCDP?.addCustomEvent)" ) DEFAULT_SERVER = object() @@ -655,15 +655,15 @@ def close(self) -> None: if self._closed: return self._closed = True + if self._launched_browser is not None: + self._launched_browser["close"]() + self._launched_browser = None try: if self.transport: self.transport.close() except Exception: pass self.transport = None - if self._launched_browser is not None: - self._launched_browser["close"]() - self._launched_browser = None for injector in self._extension_injectors: try: injector.close() diff --git a/python/modcdp/extension.zip b/python/modcdp/extension.zip index e88eba8..268e970 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 3b3bce8..e3f6bcd 100644 --- a/python/modcdp/injector/BorrowedExtensionInjector.py +++ b/python/modcdp/injector/BorrowedExtensionInjector.py @@ -105,7 +105,7 @@ def bootstrap_modcdp_server_expression() -> str: f"{installer}\n" "const ModCDP = installModCDPServer(globalThis);\n" "return {\n" - " ok: Boolean(ModCDP?.__ModCDPServerVersion === 1 && ModCDP?.handleCommand && ModCDP?.addCustomEvent),\n" + " ok: Boolean(ModCDP?.__ModCDPServerVersion >= 1 && ModCDP?.handleCommand && ModCDP?.addCustomEvent),\n" " extension_id: globalThis.chrome?.runtime?.id ?? null,\n" " has_tabs: Boolean(globalThis.chrome?.tabs?.query),\n" " has_debugger: Boolean(globalThis.chrome?.debugger?.sendCommand && globalThis.chrome?.debugger?.getTargets),\n" diff --git a/python/modcdp/injector/ExtensionInjector.py b/python/modcdp/injector/ExtensionInjector.py index ee56325..48b0ba7 100644 --- a/python/modcdp/injector/ExtensionInjector.py +++ b/python/modcdp/injector/ExtensionInjector.py @@ -18,7 +18,7 @@ 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)" + "Boolean(globalThis.ModCDP?.__ModCDPServerVersion >= 1 && globalThis.ModCDP?.handleCommand && globalThis.ModCDP?.addCustomEvent)" ) DEFAULT_CDP_SEND_TIMEOUT_MS = 10_000 DEFAULT_EXECUTION_CONTEXT_TIMEOUT_MS = 10_000 diff --git a/python/modcdp/translate/translate.py b/python/modcdp/translate/translate.py index d8d885b..06bbde5 100644 --- a/python/modcdp/translate/translate.py +++ b/python/modcdp/translate/translate.py @@ -153,8 +153,8 @@ 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 await globalThis.ModCDP.handleCommand(" - f"{json.dumps(method)}, {json.dumps(params)}, {json.dumps(session_id)}); }}" + "async function() { return JSON.stringify(await globalThis.ModCDP.handleCommand(" + f"{json.dumps(method)}, {json.dumps(params)}, {json.dumps(session_id)})); }}" ) @@ -175,6 +175,7 @@ def _wrap_service_worker_command( "unwrap": "runtime", }, ] + unwrap = "runtime" if method == "Mod.evaluate": runtime_params = _wrap_modcdp_evaluate(params, session_id, target_session_id) elif method == "Mod.addCustomCommand": @@ -183,7 +184,8 @@ def _wrap_service_worker_command( runtime_params = _wrap_modcdp_add_middleware(params) else: runtime_params = _wrap_custom_command(method, params, target_session_id or _optional_string(params, "cdpSessionId") or session_id) - return [{"method": "Runtime.callFunctionOn", "params": runtime_params, "unwrap": "runtime"}] + unwrap = "runtime_json" + return [{"method": "Runtime.callFunctionOn", "params": runtime_params, "unwrap": unwrap}] def wrap_command_if_needed( @@ -231,6 +233,9 @@ def _unwrap_evaluate_response(result: ProtocolResult) -> JsonValue: def unwrap_response_if_needed(result: ProtocolResult, unwrap: str | None = None) -> JsonValue: + if unwrap == "runtime_json": + value = _unwrap_evaluate_response(result) + return cast(JsonValue, json.loads(value)) if isinstance(value, str) else value return _unwrap_evaluate_response(result) if unwrap == "runtime" else (result or {}) diff --git a/python/modcdp/types/modcdp.py b/python/modcdp/types/modcdp.py index bea788e..1fdca59 100644 --- a/python/modcdp/types/modcdp.py +++ b/python/modcdp/types/modcdp.py @@ -108,7 +108,7 @@ class _TranslatedStepRequired(TypedDict): class TranslatedStep(_TranslatedStepRequired, total=False): params: MessageParams sessionId: str | None - unwrap: Literal["runtime"] + unwrap: Literal["runtime", "runtime_json"] class TranslatedCommand(TypedDict): diff --git a/python/pyproject.toml b/python/pyproject.toml index a3d33af..b467f44 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "modcdp" -version = "0.0.6" +version = "0.0.13" description = "Python client for ModCDP." readme = "README.md" requires-python = ">=3.11" diff --git a/python/tests/test_ExtensionInjector.py b/python/tests/test_ExtensionInjector.py index 8d8d34c..7d50e9a 100644 --- a/python/tests/test_ExtensionInjector.py +++ b/python/tests/test_ExtensionInjector.py @@ -190,7 +190,7 @@ def attach_to_target(target_id: str) -> str | None: {"expression": "globalThis.ModCDP?.__ModCDPServerVersion", "returnByValue": True}, cast(str, session_id), ) - self.assertEqual(cast(dict[str, Any], version.get("result") or {}).get("value"), 1) + self.assertEqual(cast(dict[str, Any], version.get("result") or {}).get("value"), 2) finally: injector.close() ws.close() diff --git a/python/tests/test_ReverseWebSocketUpstreamTransport.py b/python/tests/test_ReverseWebSocketUpstreamTransport.py index 15168ab..85a5f0e 100644 --- a/python/tests/test_ReverseWebSocketUpstreamTransport.py +++ b/python/tests/test_ReverseWebSocketUpstreamTransport.py @@ -1,7 +1,9 @@ from __future__ import annotations import json +import os import socket +import sys import threading import time import unittest @@ -134,7 +136,10 @@ def test_accepts_replacement_peer_after_disconnect(self) -> None: def test_accepts_real_extension_reverse_connection_and_routes_cdp_through_loopback(self) -> None: cdp = ModCDPClient( - launcher={"launcher_mode": "local", "launcher_options": {"headless": True, "sandbox": False}}, + launcher={ + "launcher_mode": "local", + "launcher_options": {"headless": sys.platform.startswith("linux") and not os.environ.get("DISPLAY"), "sandbox": False}, + }, upstream={"upstream_mode": "reversews"}, injector={ "injector_mode": "auto", diff --git a/python/tests/test_translate.py b/python/tests/test_translate.py index e5aa3fc..3fa640b 100644 --- a/python/tests/test_translate.py +++ b/python/tests/test_translate.py @@ -31,6 +31,13 @@ def test_routes_wraps_and_unwraps_modcdp_protocol_messages_deterministically(sel self.assertIn('attachToSession("session-1")', str(wrapped["steps"][0].get("params", {}).get("functionDeclaration"))) self.assertEqual(wrapped["steps"][0].get("unwrap"), "runtime") + configured = wrap_command_if_needed( + "Mod.configure", + {"server": {"server_routes": {"*.*": "loopback_cdp"}}}, + cdp_session_id="session-1", + ) + self.assertEqual(configured["steps"][0].get("unwrap"), "runtime_json") + 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 ad24459..1097ad8 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -13,7 +13,7 @@ wheels = [ [[package]] name = "modcdp" -version = "0.0.6" +version = "0.0.13" source = { editable = "." } dependencies = [ { name = "pydantic" }, diff --git a/tsconfig.json b/tsconfig.json index 763fa58..6d66b2b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,8 @@ "rootDir": ".", "types": ["node", "chrome"], "allowSyntheticDefaultImports": true, - "declaration": false, + "declaration": true, + "declarationMap": true, "esModuleInterop": true, "inlineSources": true, "noImplicitAny": false,