Skip to content

Commit de132bc

Browse files
authored
Default SEA binary server-v3 to only bind to localhost (#2091)
## Summary - default the v3 server listener to localhost unless HOST is explicitly set - warn when HOST=0.0.0.0 opts into all-interface binding - make the v4 SEA build choose a Node binary that contains the SEA fuse ## Validation - pnpm --filter @browserbasehq/stagehand-server-v3 run build:esm-tests - pnpm --filter @browserbasehq/stagehand-server-v3 run typecheck - pnpm --filter @browserbasehq/stagehand-server-v4 run typecheck - pnpm --filter @browserbasehq/stagehand-server-v3 run test:unit -- packages/server-v3/dist/tests/unit/listenHost.test.js - pnpm --filter @browserbasehq/stagehand-server-v4 run build:sea:esm -- --binary-name=stagehand-server-v4-localhost-binding-test Leaving the full suite to CI. <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Default `@browserbasehq/stagehand-server-v3` to listen on localhost unless `HOST` is set, and warn on `HOST=0.0.0.0` to prevent accidental exposure. Also make the `@browserbasehq/stagehand-server-v4` SEA build use a Node binary that includes the SEA fuse for reliable builds. - New Features - Default listen host to `localhost` when `HOST` is unset or blank; respect explicit values. - Warn when `HOST=0.0.0.0` to make all-interface binding explicit. - Migration - If you need external access, set `HOST=0.0.0.0` (or a specific interface). <sup>Written for commit 3cecdce. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. -->
1 parent a11603d commit de132bc

5 files changed

Lines changed: 102 additions & 4 deletions

File tree

.changeset/local-host-default.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@browserbasehq/stagehand-server-v3": patch
3+
---
4+
5+
Default the v3 server listener to localhost, respect explicit HOST values, and warn when HOST=0.0.0.0 exposes all interfaces.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
export const DEFAULT_LISTEN_HOST = "localhost";
2+
export const ALL_INTERFACES_LISTEN_HOST = "0.0.0.0";
3+
4+
const allInterfacesWarning =
5+
"HOST=0.0.0.0 was passed explicitly, so the Stagehand server will listen on all network interfaces. Use HOST=localhost or HOST=127.0.0.1 unless you intend to expose this server beyond the local machine.";
6+
7+
export type ListenHostConfig = {
8+
host: string;
9+
warning?: string;
10+
};
11+
12+
export const getListenHostConfig = (
13+
hostEnv = process.env.HOST,
14+
): ListenHostConfig => {
15+
const host = hostEnv?.trim() || DEFAULT_LISTEN_HOST;
16+
17+
if (host === ALL_INTERFACES_LISTEN_HOST) {
18+
return {
19+
host,
20+
warning: allInterfacesWarning,
21+
};
22+
}
23+
24+
return { host };
25+
};

packages/server-v3/src/server.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
import { StatusCodes } from "http-status-codes";
1818

1919
import { logging } from "./lib/logging/index.js";
20+
import { getListenHostConfig } from "./lib/listenHost.js";
2021
import {
2122
destroySessionStore,
2223
initializeSessionStore,
@@ -258,8 +259,13 @@ const start = async () => {
258259
appWithTypes.route(readinessRoute);
259260
await app.ready();
260261

262+
const listenHost = getListenHostConfig();
263+
if (listenHost.warning) {
264+
app.log.warn(listenHost.warning);
265+
}
266+
261267
await app.listen({
262-
host: "0.0.0.0",
268+
host: listenHost.host,
263269
port: parseInt(process.env.PORT ?? "3000", 10),
264270
});
265271
console.log("Routes registered:", app.printRoutes());
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import assert from "node:assert/strict";
2+
import { describe, it } from "node:test";
3+
4+
import {
5+
ALL_INTERFACES_LISTEN_HOST,
6+
DEFAULT_LISTEN_HOST,
7+
getListenHostConfig,
8+
} from "../../src/lib/listenHost.js";
9+
10+
describe("getListenHostConfig", () => {
11+
it("defaults to localhost when HOST is not provided", () => {
12+
assert.deepEqual(getListenHostConfig(undefined), {
13+
host: DEFAULT_LISTEN_HOST,
14+
});
15+
});
16+
17+
it("defaults to localhost when HOST is blank", () => {
18+
assert.deepEqual(getListenHostConfig(" "), {
19+
host: DEFAULT_LISTEN_HOST,
20+
});
21+
});
22+
23+
it("respects explicit localhost host values", () => {
24+
assert.deepEqual(getListenHostConfig("localhost"), {
25+
host: "localhost",
26+
});
27+
assert.deepEqual(getListenHostConfig("127.0.0.1"), {
28+
host: "127.0.0.1",
29+
});
30+
});
31+
32+
it("warns when HOST explicitly requests all interfaces", () => {
33+
const config = getListenHostConfig(ALL_INTERFACES_LISTEN_HOST);
34+
35+
assert.equal(config.host, ALL_INTERFACES_LISTEN_HOST);
36+
assert.match(config.warning ?? "", /listen on all network interfaces/);
37+
});
38+
});

packages/server-v4/scripts/build-sea.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import esbuild from "esbuild";
2323
import { getRepoRootDir } from "./runtimePaths.js";
2424

2525
const repoDir = getRepoRootDir();
26+
const seaFuse = "NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2";
2627

2728
const argValue = (name: string) => {
2829
const prefix = `--${name}=`;
@@ -119,6 +120,14 @@ const runOptional = (
119120
spawnSync(cmd, args, { stdio: "ignore", ...opts });
120121
};
121122

123+
const hasSeaFuse = (binaryPath: string): boolean => {
124+
try {
125+
return fs.readFileSync(binaryPath).includes(Buffer.from(seaFuse));
126+
} catch {
127+
return false;
128+
}
129+
};
130+
122131
const download = (url: string, dest: string): Promise<void> =>
123132
new Promise((resolve, reject) => {
124133
https
@@ -167,15 +176,20 @@ const resolveNodeBinary = async (): Promise<string> => {
167176
`Cross-platform builds are not supported. Host=${process.platform}, target=${targetPlatform}`,
168177
);
169178
}
170-
if (targetArch === process.arch) {
179+
if (targetArch === process.arch && hasSeaFuse(process.execPath)) {
171180
return process.execPath;
172181
}
182+
if (targetArch === process.arch) {
183+
console.warn(
184+
`Current Node binary at ${process.execPath} does not include ${seaFuse}; falling back to the official ${process.version} distribution for SEA injection.`,
185+
);
186+
}
173187

174188
const version = process.version;
175189
const distPlatform = targetPlatform === "win32" ? "win" : targetPlatform;
176190
const archiveBase = `node-${version}-${distPlatform}-${targetArch}`;
177191
const archiveExt = distPlatform === "win" ? "zip" : "tar.xz";
178-
const tmpRoot = `${os.tmpdir()}/stagehand-sea/${archiveBase}`;
192+
const tmpRoot = `${os.tmpdir()}/stagehand-server-v4-sea/${archiveBase}`;
179193
const archivePath = `${tmpRoot}/${archiveBase}.${archiveExt}`;
180194
const extractRoot = `${tmpRoot}/${archiveBase}`;
181195
const binaryPath =
@@ -184,6 +198,11 @@ const resolveNodeBinary = async (): Promise<string> => {
184198
: `${extractRoot}/bin/node`;
185199

186200
if (fs.existsSync(binaryPath)) {
201+
if (!hasSeaFuse(binaryPath)) {
202+
throw new Error(
203+
`Node binary at ${binaryPath} does not include ${seaFuse}; unable to build SEA binary. Delete ${tmpRoot} and retry.`,
204+
);
205+
}
187206
return binaryPath;
188207
}
189208

@@ -208,6 +227,11 @@ const resolveNodeBinary = async (): Promise<string> => {
208227
if (!fs.existsSync(binaryPath)) {
209228
throw new Error(`Missing Node binary at ${binaryPath}`);
210229
}
230+
if (!hasSeaFuse(binaryPath)) {
231+
throw new Error(
232+
`Node binary at ${binaryPath} does not include ${seaFuse}; unable to build SEA binary. Delete ${tmpRoot} and retry.`,
233+
);
234+
}
211235
return binaryPath;
212236
};
213237

@@ -477,7 +501,7 @@ const main = async () => {
477501
"NODE_SEA_BLOB",
478502
`${repoDir}/packages/server-v4/dist/sea/sea-prep.blob`,
479503
"--sentinel-fuse",
480-
"NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2",
504+
seaFuse,
481505
];
482506
if (targetPlatform === "darwin") {
483507
postjectArgs.push("--macho-segment-name", "NODE_SEA");

0 commit comments

Comments
 (0)