Skip to content

Add runtime-specific TypeScript SDK exports - #2420

Open
monadoid wants to merge 1 commit into
remove-node-dependenciesfrom
add-runtime-specific-sdk-exports
Open

Add runtime-specific TypeScript SDK exports#2420
monadoid wants to merge 1 commit into
remove-node-dependenciesfrom
add-runtime-specific-sdk-exports

Conversation

@monadoid

@monadoid monadoid commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

In order to launch local Chrome, stagehand currently needs Node. But if you’re using any remote browser, the SDK itself has no Node dependencies.

This allows remote-browser users to safely use Stagehand from Bun, Deno, Workers, browsers, and other web-compatible runtimes without changing how they install or import it:

npm install @browserbasehq/stagehand
import { Stagehand } from "@browserbasehq/stagehand";

The correct implementation is selected automatically through standard package conditional exports:

{
  "exports": {
    ".": {
      "workerd": "./dist/web/index.mjs",
      "deno": "./dist/web/index.mjs",
      "bun": "./dist/web/index.mjs",
      "browser": "./dist/web/index.mjs",
      "node": "./dist/node/index.mjs",
      "default": "./dist/web/index.mjs"
    }
  }
}

In Node, users can launch a local browser:

const stagehand = new Stagehand({
  browser: {
    type: "local",
  },
});

Outside Node, local is excluded from the exported TypeScript type and Zod schema, so they can't do this:

const stagehand = new Stagehand({
  browser: {
    type: "local", // Type error: "local" is not an available browser source
  },
});

@changeset-bot

changeset-bot Bot commented Jul 25, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: de333b1

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 26 files

Confidence score: 2/5

  • In packages/sdk-ts/src/runtime/node/localBrowserLauncher.ts, launching a local browser breaks when the SDK install path contains spaces because the extension directory is passed as a percent-encoded URL path, so Chrome cannot load it; users in common workspace setups will hit startup failures — decode/convert the file URL to a real filesystem path before passing it to Chrome.
  • In packages/sdk-ts/src/browserSource.shared.ts, authenticated CDP flows drop browser.headers, so Stagehand.init() cannot connect to protected CDP endpoints and initialization fails for secured environments — thread cdpHeaders through Stagehand, RPCClient, and CDP discovery/connection calls.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/sdk-ts/src/runtime/node/localBrowserLauncher.ts">

<violation number="1" location="packages/sdk-ts/src/runtime/node/localBrowserLauncher.ts:68">
P1: Local browser launch fails when the installed SDK path contains spaces: Chrome receives a percent-encoded extension path such as `consumer%20with%20spaces/.../extension`, which is not a directory. Convert the file URL with Node's `fileURLToPath` instead of reading `.pathname`.</violation>
</file>

<file name="packages/sdk-ts/src/browserSource.shared.ts">

<violation number="1" location="packages/sdk-ts/src/browserSource.shared.ts:64">
P1: CDP connections requiring authentication ignore the supplied `browser.headers`, so `Stagehand.init()` cannot connect to protected CDP endpoints. Thread `cdpHeaders` through `Stagehand`, `RPCClient`, and CDP discovery/WebSocket transport, or reject/remove this currently nonfunctional option.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Consumer as Consumer Code
    participant PkgRes as Node Package Resolution
    participant Export as package.json exports
    participant NodeEntry as dist/node/index.js
    participant WebEntry as dist/web/index.js
    participant NodeSH as Stagehand (Node)
    participant WebSH as Stagehand (Web)
    participant Shared as StagehandCore
    participant NodeBS as Node browserSource
    participant WebBS as Web browserSource
    participant RemoteBS as resolveRemoteBrowserSource
    participant LocalLaunch as launchLocalBrowser
    participant Protocol as Protocol/Backend

    Note over Consumer,Protocol: NEW: Dual-build package with conditional exports

    Consumer->>PkgRes: import { Stagehand } from "@browserbasehq/stagehand"
    PkgRes->>Export: Check runtime conditions
    alt Runtime is Node.js
        Export-->>Consumer: dist/node/index.js (type: "node")
        Consumer->>NodeEntry: import resolved
        NodeEntry->>NodeSH: new Stagehand(initParams)
        NodeSH->>Shared: super(initParams, parsedInit, { resolveBrowserSource })
    else Runtime is workerd/deno/bun/browser/default
        Export-->>Consumer: dist/web/index.js (type: "workerd"/etc)
        Consumer->>WebEntry: import resolved
        WebEntry->>WebSH: new Stagehand(initParams)
        WebSH->>Shared: super(initParams, parsedInit, { resolveBrowserSource })
    end

    Note over Consumer,Protocol: Init flow with runtime-specific browser source resolution

    Consumer->>Shared: init(params)
    Shared->>Shared: Parse initParams with runtime-specific schema
    alt Node variant (has "local")
        Shared->>NodeBS: resolveBrowserSource(parsed)
        alt browser.type === "local"
            NodeBS->>LocalLaunch: launchLocalBrowser(options)
            LocalLaunch-->>NodeBS: CDP URL + close
        else browser.type is "browserbase" or "cdp"
            NodeBS->>RemoteBS: resolveRemoteBrowserSource(parsed, deps)
        end
    else Web variant (no "local")
        Shared->>WebBS: resolveBrowserSource(parsed)
        WebBS->>RemoteBS: resolveRemoteBrowserSource(parsed, deps)
    end

    Note over Consumer,Protocol: Browser source resolution details

    alt browser.type === "browserbase"
        RemoteBS->>Protocol: createBrowserbaseSession()
        Protocol-->>RemoteBS: session info (cdpUrl, sessionId)
        RemoteBS-->>Shared: ResolvedBrowserSource
    else browser.type === "cdp"
        RemoteBS-->>Shared: ResolvedBrowserSource (cdpUrl, headers)
    end

    Note over Consumer,Protocol: TypeScript type safety differs by runtime

    alt Node variant
        Consumer->>NodeSH: initParams type includes { type: "local" }
        NodeSH-->>Consumer: OK (type and runtime both support local)
    else Web variant
        Consumer->>WebSH: initParams type only includes "browserbase" | "cdp"
        Consumer->>Consumer: BROWSER ERROR if user passes { type: "local" }
    end

    Note over Consumer,Protocol: Runtime portability enforcement

    Consumer->>Consumer: Static import resolves to web variant
    alt User attempts local browser in web runtime
        Consumer->>WebBS: resolveBrowserSource({ browser: { type: "local" } })
        WebBS->>WebBS: Schema validation fails (type not in union)
        WebBS-->>Shared: Throws Zod validation error
    end
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

const STAGEHAND_EXTENSION_PATH = new URL(
import.meta.url.includes("/dist/node/") ? "./extension" : "../../../../server/dist",
import.meta.url,
).pathname;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Local browser launch fails when the installed SDK path contains spaces: Chrome receives a percent-encoded extension path such as consumer%20with%20spaces/.../extension, which is not a directory. Convert the file URL with Node's fileURLToPath instead of reading .pathname.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/sdk-ts/src/runtime/node/localBrowserLauncher.ts, line 68:

<comment>Local browser launch fails when the installed SDK path contains spaces: Chrome receives a percent-encoded extension path such as `consumer%20with%20spaces/.../extension`, which is not a directory. Convert the file URL with Node's `fileURLToPath` instead of reading `.pathname`.</comment>

<file context>
@@ -62,7 +62,10 @@ const DEFAULT_CHROME_FLAGS = [
+const STAGEHAND_EXTENSION_PATH = new URL(
+  import.meta.url.includes("/dist/node/") ? "./extension" : "../../../../server/dist",
+  import.meta.url,
+).pathname;
 
 export async function launchLocalBrowser(
</file context>


return {
cdpUrl: browser.cdpUrl,
...(browser.headers === undefined ? {} : { cdpHeaders: browser.headers }),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: CDP connections requiring authentication ignore the supplied browser.headers, so Stagehand.init() cannot connect to protected CDP endpoints. Thread cdpHeaders through Stagehand, RPCClient, and CDP discovery/WebSocket transport, or reject/remove this currently nonfunctional option.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/sdk-ts/src/browserSource.shared.ts, line 64:

<comment>CDP connections requiring authentication ignore the supplied `browser.headers`, so `Stagehand.init()` cannot connect to protected CDP endpoints. Thread `cdpHeaders` through `Stagehand`, `RPCClient`, and CDP discovery/WebSocket transport, or reject/remove this currently nonfunctional option.</comment>

<file context>
@@ -0,0 +1,67 @@
+
+  return {
+    cdpUrl: browser.cdpUrl,
+    ...(browser.headers === undefined ? {} : { cdpHeaders: browser.headers }),
+    keepAlive: true,
+  };
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant