browse 0.9.0 → 0.9.1-alpha-39d7638
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/base.js +10 -0
- package/dist/commands/cloud/sessions/create.js +35 -0
- package/dist/commands/open.js +1 -0
- package/dist/lib/cloud/api.js +22 -0
- package/dist/lib/driver/command-cli.js +8 -2
- package/dist/lib/driver/daemon/client.js +3 -0
- package/dist/lib/driver/daemon/forwarded-env.js +39 -0
- package/dist/lib/driver/daemon/protocol.js +3 -0
- package/dist/lib/driver/daemon/server.js +2 -0
- package/dist/lib/driver/doctor.js +34 -1
- package/dist/lib/driver/flags.js +6 -0
- package/dist/lib/driver/mode.js +24 -0
- package/dist/lib/driver/remote.disabled.js +8 -1
- package/dist/lib/driver/remote.js +37 -5
- package/dist/lib/driver/session-manager.js +55 -1
- package/dist/lib/identity.js +257 -0
- package/dist/lib/telemetry.js +2 -41
- package/oclif.manifest.json +887 -490
- package/package.json +1 -1
- package/skills/browse/SKILL.md +4 -1
package/dist/base.js
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { Command } from "@oclif/core";
|
|
2
2
|
import { CommandFailure } from "./lib/errors.js";
|
|
3
|
+
import { setCliVersion } from "./lib/identity.js";
|
|
3
4
|
import { recordCommandError } from "./lib/telemetry.js";
|
|
4
5
|
export class BrowseCommand extends Command {
|
|
6
|
+
async init() {
|
|
7
|
+
await super.init();
|
|
8
|
+
// Seed the CLI version from oclif's Config (the single source of truth) so
|
|
9
|
+
// non-command contexts — remote session userMetadata and cloud API headers
|
|
10
|
+
// — can stamp the real version without any filesystem read. This runs in
|
|
11
|
+
// every process before run(), including the background `browse daemon` that
|
|
12
|
+
// creates Browserbase sessions, so cli_version never regresses to "unknown".
|
|
13
|
+
setCliVersion(this.config.version);
|
|
14
|
+
}
|
|
5
15
|
async catch(err) {
|
|
6
16
|
if (err instanceof CommandFailure) {
|
|
7
17
|
recordCommandError("runtime", "COMMAND_FAILURE", err.telemetry);
|
|
@@ -2,6 +2,7 @@ import { Flags } from "@oclif/core";
|
|
|
2
2
|
import { createBrowserbaseClient, deepMerge, outputJson, resolveBody, withBrowserbaseApi, } from "../../../lib/cloud/api.js";
|
|
3
3
|
import { apiCommonFlags, toApiOptions } from "../../../lib/cloud/flags.js";
|
|
4
4
|
import { fail } from "../../../lib/errors.js";
|
|
5
|
+
import { getCliVersion, resolveInstallId, toMetadataValue, } from "../../../lib/identity.js";
|
|
5
6
|
import { BrowseCommand } from "../../../base.js";
|
|
6
7
|
const REGIONS = [
|
|
7
8
|
"us-west-2",
|
|
@@ -56,6 +57,39 @@ function buildSessionCreateBody(flags) {
|
|
|
56
57
|
}
|
|
57
58
|
return body;
|
|
58
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Stamp anonymous CLI attribution onto the session-create `userMetadata` so
|
|
62
|
+
* every CLI-created cloud session is attributable to the CLI (matching the
|
|
63
|
+
* driver `open --remote` path). Any user-supplied `userMetadata` (via --body or
|
|
64
|
+
* --stdin) is preserved; our attribution keys (browse_cli/install_id/
|
|
65
|
+
* cli_version) are authoritative and override caller values for those keys.
|
|
66
|
+
*
|
|
67
|
+
* Resolving the install id is best-effort and never throws; if it can't be
|
|
68
|
+
* resolved we still send browse_cli + cli_version. Values are run through
|
|
69
|
+
* toMetadataValue() so the session-create validator never 400s on a stray
|
|
70
|
+
* character or an over-length value.
|
|
71
|
+
*/
|
|
72
|
+
async function applyCliAttribution(body) {
|
|
73
|
+
const rawExisting = body.userMetadata && typeof body.userMetadata === "object"
|
|
74
|
+
? body.userMetadata
|
|
75
|
+
: {};
|
|
76
|
+
// Strip any caller-supplied install_id before merging so it cannot be
|
|
77
|
+
// spoofed when resolution fails (our authoritative value is set below).
|
|
78
|
+
const existing = Object.fromEntries(Object.entries(rawExisting).filter(([k]) => k !== "install_id"));
|
|
79
|
+
const userMetadata = {
|
|
80
|
+
...existing,
|
|
81
|
+
browse_cli: "true",
|
|
82
|
+
cli_version: toMetadataValue(getCliVersion()),
|
|
83
|
+
};
|
|
84
|
+
const installId = await resolveInstallId(process.env).catch(() => undefined);
|
|
85
|
+
if (installId) {
|
|
86
|
+
const sanitized = toMetadataValue(installId);
|
|
87
|
+
if (sanitized) {
|
|
88
|
+
userMetadata.install_id = sanitized;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
body.userMetadata = userMetadata;
|
|
92
|
+
}
|
|
59
93
|
export default class SessionsCreate extends BrowseCommand {
|
|
60
94
|
static description = "Create a new browser session. Use flags for common options, or --body/--stdin for the full API.";
|
|
61
95
|
static examples = [
|
|
@@ -136,6 +170,7 @@ export default class SessionsCreate extends BrowseCommand {
|
|
|
136
170
|
});
|
|
137
171
|
const flagBody = buildSessionCreateBody(flags);
|
|
138
172
|
const body = deepMerge(jsonBody, flagBody);
|
|
173
|
+
await applyCliAttribution(body);
|
|
139
174
|
outputJson(await client.sessions.create(body));
|
|
140
175
|
});
|
|
141
176
|
}
|
package/dist/commands/open.js
CHANGED
|
@@ -7,6 +7,7 @@ export default class Open extends BrowseCommand {
|
|
|
7
7
|
"browse open https://example.com",
|
|
8
8
|
"browse open https://example.com --local --headed",
|
|
9
9
|
"browse open https://example.com --remote",
|
|
10
|
+
"browse open https://example.com --remote --verified --proxies",
|
|
10
11
|
"browse open https://example.com --auto-connect",
|
|
11
12
|
"browse open https://example.com --cdp 9222",
|
|
12
13
|
"browse open https://example.com --cdp ws://127.0.0.1:9222/devtools/browser/<id> --target-id <target-id>",
|
package/dist/lib/cloud/api.js
CHANGED
|
@@ -4,10 +4,30 @@ import { access, mkdir, writeFile } from "node:fs/promises";
|
|
|
4
4
|
import { dirname, resolve } from "node:path";
|
|
5
5
|
import { Readable } from "node:stream";
|
|
6
6
|
import { CommandFailure, fail } from "../errors.js";
|
|
7
|
+
import { getCliVersion, peekInstallId, resolveInstallId } from "../identity.js";
|
|
7
8
|
import { setRunTelemetryCompletion } from "../run-telemetry.js";
|
|
8
9
|
export { outputJson } from "../output.js";
|
|
9
10
|
const defaultBrowserbaseApiUrl = "https://api.browserbase.com";
|
|
10
11
|
const browserbaseSettingsUrl = "https://browserbase.com/settings";
|
|
12
|
+
// Kick off install-id resolution at module load so the value is usually cached
|
|
13
|
+
// by the time a cloud request runs. Resolution is best-effort and never throws.
|
|
14
|
+
void resolveInstallId(process.env).catch(() => undefined);
|
|
15
|
+
/**
|
|
16
|
+
* Attribution headers stamped onto cloud (Search/Fetch) requests so CLI usage
|
|
17
|
+
* is attributable to an install. `x-bb-client` is always sent;
|
|
18
|
+
* `x-bb-install-id` is included only when already resolved (we never block a
|
|
19
|
+
* request on disk I/O) and never sent as an empty string.
|
|
20
|
+
*/
|
|
21
|
+
function attributionHeaders() {
|
|
22
|
+
const headers = {
|
|
23
|
+
"x-bb-client": `browse-cli/${getCliVersion()}`,
|
|
24
|
+
};
|
|
25
|
+
const installId = peekInstallId();
|
|
26
|
+
if (installId) {
|
|
27
|
+
headers["x-bb-install-id"] = installId;
|
|
28
|
+
}
|
|
29
|
+
return headers;
|
|
30
|
+
}
|
|
11
31
|
export function resolveApiKey(args) {
|
|
12
32
|
const apiKey = args.apiKey ?? process.env.BROWSERBASE_API_KEY;
|
|
13
33
|
return (apiKey ||
|
|
@@ -32,6 +52,7 @@ export function createBrowserbaseClient(args) {
|
|
|
32
52
|
return new Browserbase({
|
|
33
53
|
apiKey: resolveApiKey(args),
|
|
34
54
|
baseURL: resolveBaseUrl(args),
|
|
55
|
+
defaultHeaders: attributionHeaders(),
|
|
35
56
|
});
|
|
36
57
|
}
|
|
37
58
|
export async function withBrowserbaseApi(command, operation) {
|
|
@@ -104,6 +125,7 @@ export async function requestBrowserbase(args, pathname, init = {}) {
|
|
|
104
125
|
...init,
|
|
105
126
|
headers: {
|
|
106
127
|
"x-bb-api-key": resolveApiKey(args),
|
|
128
|
+
...attributionHeaders(),
|
|
107
129
|
...(init.headers ?? {}),
|
|
108
130
|
},
|
|
109
131
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Flags } from "@oclif/core";
|
|
2
|
-
import { autoConnectFlag, cdpFlag, chromeArgFlag, headedFlag, headlessFlag, ignoreDefaultChromeArgFlag, localFlag, noDefaultChromeArgsFlag, remoteFlag, sessionFlag, sessionName, targetIdFlag, } from "./flags.js";
|
|
2
|
+
import { autoConnectFlag, cdpFlag, chromeArgFlag, headedFlag, headlessFlag, ignoreDefaultChromeArgFlag, localFlag, noDefaultChromeArgsFlag, proxiesFlag, remoteFlag, sessionFlag, sessionName, targetIdFlag, verifiedFlag, } from "./flags.js";
|
|
3
3
|
import { getDriverStatus } from "./daemon/client.js";
|
|
4
4
|
import { hasChromeArgFlags, resolveConnectionTarget, } from "./mode.js";
|
|
5
5
|
import { outputJson } from "../output.js";
|
|
@@ -13,9 +13,11 @@ export const driverCommandFlags = {
|
|
|
13
13
|
"ignore-default-chrome-arg": ignoreDefaultChromeArgFlag,
|
|
14
14
|
local: localFlag,
|
|
15
15
|
"no-default-chrome-args": noDefaultChromeArgsFlag,
|
|
16
|
+
proxies: proxiesFlag,
|
|
16
17
|
remote: remoteFlag,
|
|
17
18
|
session: sessionFlag,
|
|
18
19
|
"target-id": targetIdFlag,
|
|
20
|
+
verified: verifiedFlag,
|
|
19
21
|
};
|
|
20
22
|
export const waitUntilFlag = Flags.string({
|
|
21
23
|
default: "load",
|
|
@@ -58,7 +60,9 @@ export function hasExplicitDriverTarget(flags) {
|
|
|
58
60
|
hasChromeArgFlags(flags) ||
|
|
59
61
|
flags["target-id"] ||
|
|
60
62
|
flags.headed ||
|
|
61
|
-
flags.headless
|
|
63
|
+
flags.headless ||
|
|
64
|
+
flags.verified ||
|
|
65
|
+
flags.proxies);
|
|
62
66
|
}
|
|
63
67
|
function hasModeOnlyFlag(flags) {
|
|
64
68
|
return Boolean((flags.local || flags.remote) &&
|
|
@@ -68,6 +72,8 @@ function hasModeOnlyFlag(flags) {
|
|
|
68
72
|
!flags["target-id"] &&
|
|
69
73
|
!flags.headed &&
|
|
70
74
|
!flags.headless &&
|
|
75
|
+
!flags.verified &&
|
|
76
|
+
!flags.proxies &&
|
|
71
77
|
flags.local !== flags.remote);
|
|
72
78
|
}
|
|
73
79
|
function targetMatchesRequestedMode(target, flags) {
|
|
@@ -4,6 +4,7 @@ import net from "node:net";
|
|
|
4
4
|
import { CommandFailure, fail } from "../../errors.js";
|
|
5
5
|
import { targetsCompatible } from "../mode.js";
|
|
6
6
|
import { cleanupDaemonFiles, ensureRuntimeDir, getLockPath, getPidPath, getSocketPath, PRIVATE_FILE_MODE, } from "./paths.js";
|
|
7
|
+
import { collectForwardedEnv } from "./forwarded-env.js";
|
|
7
8
|
import { isProcessAlive } from "./process.js";
|
|
8
9
|
import { ResponseSchema } from "./protocol.js";
|
|
9
10
|
export async function ensureDriverDaemon({ session, target, }) {
|
|
@@ -36,6 +37,7 @@ export async function ensureDriverDaemon({ session, target, }) {
|
|
|
36
37
|
export async function openViaDaemon(session, url, options = {}) {
|
|
37
38
|
return sendDriverRequest(session, {
|
|
38
39
|
...options,
|
|
40
|
+
forwardedEnv: await collectForwardedEnv(),
|
|
39
41
|
id: requestId(),
|
|
40
42
|
type: "open",
|
|
41
43
|
url,
|
|
@@ -44,6 +46,7 @@ export async function openViaDaemon(session, url, options = {}) {
|
|
|
44
46
|
export async function runDriverCommandViaDaemon(session, command, params) {
|
|
45
47
|
return sendDriverRequest(session, {
|
|
46
48
|
command,
|
|
49
|
+
forwardedEnv: await collectForwardedEnv(),
|
|
47
50
|
id: requestId(),
|
|
48
51
|
params,
|
|
49
52
|
type: "command",
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { getRemote } from "../remote-binding.js";
|
|
3
|
+
/** Collect the forwardable env vars that are set in the caller's env. */
|
|
4
|
+
export async function collectForwardedEnv(env = process.env) {
|
|
5
|
+
const keys = (await getRemote()).forwardedEnvKeys();
|
|
6
|
+
const forwardedEnv = {};
|
|
7
|
+
for (const key of keys) {
|
|
8
|
+
const value = env[key];
|
|
9
|
+
if (typeof value === "string" && value.length > 0) {
|
|
10
|
+
forwardedEnv[key] = value;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return Object.keys(forwardedEnv).length > 0 ? forwardedEnv : undefined;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Stable, secret-free fingerprint of a forwarded env set, used only to
|
|
17
|
+
* detect whether the caller's forwarded env changed between requests (so a cold
|
|
18
|
+
* session can bust its cached init-failure backoff and retry with the new key).
|
|
19
|
+
* Hashing keeps the raw key out of any retained field. Iterates the received
|
|
20
|
+
* object's own keys — the client already filtered to the forwardable set — so
|
|
21
|
+
* this carries no literal key names. Returns "" for an empty/absent set.
|
|
22
|
+
*/
|
|
23
|
+
export function forwardedEnvSignature(forwardedEnv) {
|
|
24
|
+
if (!forwardedEnv)
|
|
25
|
+
return "";
|
|
26
|
+
const entries = Object.entries(forwardedEnv)
|
|
27
|
+
.filter(([, value]) => typeof value === "string" && value.length > 0)
|
|
28
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
29
|
+
if (entries.length === 0)
|
|
30
|
+
return "";
|
|
31
|
+
const hash = createHash("sha256");
|
|
32
|
+
for (const [key, value] of entries) {
|
|
33
|
+
hash.update(key);
|
|
34
|
+
hash.update("\0");
|
|
35
|
+
hash.update(value);
|
|
36
|
+
hash.update("\0");
|
|
37
|
+
}
|
|
38
|
+
return hash.digest("hex");
|
|
39
|
+
}
|
|
@@ -2,6 +2,9 @@ import { z } from "zod";
|
|
|
2
2
|
import { DriverCommandNameSchema } from "../commands/types.js";
|
|
3
3
|
const RequestBaseSchema = z.object({
|
|
4
4
|
id: z.string().min(1),
|
|
5
|
+
// Env vars forwarded from the caller's env so a key set after the daemon
|
|
6
|
+
// started is still honored. See ./forwarded-env.ts.
|
|
7
|
+
forwardedEnv: z.record(z.string(), z.string()).optional(),
|
|
5
8
|
});
|
|
6
9
|
export const OpenRequestSchema = RequestBaseSchema.extend({
|
|
7
10
|
type: z.literal("open"),
|
|
@@ -89,6 +89,7 @@ async function handleLine(line, socket, manager) {
|
|
|
89
89
|
}
|
|
90
90
|
try {
|
|
91
91
|
if (request.type === "open") {
|
|
92
|
+
manager.applyForwardedEnv(request.forwardedEnv);
|
|
92
93
|
await writeResponse(socket, {
|
|
93
94
|
data: await manager.execute("open", {
|
|
94
95
|
timeoutMs: request.timeoutMs,
|
|
@@ -101,6 +102,7 @@ async function handleLine(line, socket, manager) {
|
|
|
101
102
|
return false;
|
|
102
103
|
}
|
|
103
104
|
if (request.type === "command") {
|
|
105
|
+
manager.applyForwardedEnv(request.forwardedEnv);
|
|
104
106
|
await writeResponse(socket, {
|
|
105
107
|
data: await manager.execute(request.command, request.params),
|
|
106
108
|
id: request.id,
|
|
@@ -44,6 +44,27 @@ export async function buildDoctorReport(options, deps = {}) {
|
|
|
44
44
|
if (!checks.some((check) => check.name === "daemon")) {
|
|
45
45
|
checks.push(await daemonCheck(options.session, status, paths, deps));
|
|
46
46
|
}
|
|
47
|
+
if (status?.browserbaseSessionId) {
|
|
48
|
+
const { browserbaseSessionId, browserbaseSessionUrl, browserbaseDebugUrl } = status;
|
|
49
|
+
const details = {
|
|
50
|
+
sessionId: browserbaseSessionId,
|
|
51
|
+
};
|
|
52
|
+
if (browserbaseSessionUrl) {
|
|
53
|
+
details.sessionUrl = browserbaseSessionUrl;
|
|
54
|
+
}
|
|
55
|
+
if (browserbaseDebugUrl) {
|
|
56
|
+
details.liveViewUrl = browserbaseDebugUrl;
|
|
57
|
+
}
|
|
58
|
+
const message = browserbaseSessionUrl
|
|
59
|
+
? `session ${browserbaseSessionId} — ${browserbaseSessionUrl}`
|
|
60
|
+
: `session ${browserbaseSessionId}`;
|
|
61
|
+
checks.push({
|
|
62
|
+
details,
|
|
63
|
+
message,
|
|
64
|
+
name: "browserbase",
|
|
65
|
+
status: "ok",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
47
68
|
const explicitTarget = hasExplicitDriverTarget(options.flags);
|
|
48
69
|
let target;
|
|
49
70
|
let targetFailed = false;
|
|
@@ -271,8 +292,13 @@ function nextStep(verdict, checks, target, flags, session, status) {
|
|
|
271
292
|
: `browse status --session ${session}`;
|
|
272
293
|
}
|
|
273
294
|
const parts = ["browse open", DEFAULT_URL];
|
|
274
|
-
if (target.kind === "remote")
|
|
295
|
+
if (target.kind === "remote") {
|
|
275
296
|
parts.push("--remote");
|
|
297
|
+
if (target.verified)
|
|
298
|
+
parts.push("--verified");
|
|
299
|
+
if (target.proxies)
|
|
300
|
+
parts.push("--proxies");
|
|
301
|
+
}
|
|
276
302
|
if (target.kind === "auto-connect")
|
|
277
303
|
parts.push("--auto-connect");
|
|
278
304
|
if (target.kind === "cdp") {
|
|
@@ -299,6 +325,13 @@ function formatTarget(target) {
|
|
|
299
325
|
return `managed-local, ${target.headless ? "headless" : "headed"}`;
|
|
300
326
|
if (target.kind === "cdp")
|
|
301
327
|
return target.targetId ? `cdp, target ${target.targetId}` : "cdp";
|
|
328
|
+
if (target.kind === "remote") {
|
|
329
|
+
const settings = [
|
|
330
|
+
target.verified ? "verified" : null,
|
|
331
|
+
target.proxies ? "proxies" : null,
|
|
332
|
+
].filter((setting) => Boolean(setting));
|
|
333
|
+
return settings.length > 0 ? `remote (${settings.join(", ")})` : "remote";
|
|
334
|
+
}
|
|
302
335
|
return target.kind;
|
|
303
336
|
}
|
|
304
337
|
function formatDiscoverySource(discovered) {
|
package/dist/lib/driver/flags.js
CHANGED
|
@@ -16,6 +16,12 @@ export const localFlag = Flags.boolean({
|
|
|
16
16
|
export const remoteFlag = Flags.boolean({
|
|
17
17
|
description: "Use a remote Browserbase browser session.",
|
|
18
18
|
});
|
|
19
|
+
export const verifiedFlag = Flags.boolean({
|
|
20
|
+
description: "Open the remote session as a Verified (advanced-stealth) browser. Requires --remote and a Browserbase Scale plan.",
|
|
21
|
+
});
|
|
22
|
+
export const proxiesFlag = Flags.boolean({
|
|
23
|
+
description: "Route the remote session through Browserbase proxies. Requires --remote.",
|
|
24
|
+
});
|
|
19
25
|
export const autoConnectFlag = Flags.boolean({
|
|
20
26
|
description: "Auto-discover and attach to a local browser with remote debugging enabled.",
|
|
21
27
|
});
|
package/dist/lib/driver/mode.js
CHANGED
|
@@ -25,8 +25,25 @@ function chromeArgFlagsInUse(flags) {
|
|
|
25
25
|
names.push("--no-default-chrome-args");
|
|
26
26
|
return names;
|
|
27
27
|
}
|
|
28
|
+
export function remoteOnlyFlagsInUse(flags) {
|
|
29
|
+
const names = [];
|
|
30
|
+
if (flags.verified)
|
|
31
|
+
names.push("--verified");
|
|
32
|
+
if (flags.proxies)
|
|
33
|
+
names.push("--proxies");
|
|
34
|
+
return names;
|
|
35
|
+
}
|
|
28
36
|
export async function resolveConnectionTarget(flags) {
|
|
29
37
|
const chromeArgFlags = chromeArgFlagsInUse(flags);
|
|
38
|
+
// --verified / --proxies configure a Browserbase session at creation time, so
|
|
39
|
+
// they only mean something for an explicit --remote session. We deliberately
|
|
40
|
+
// do NOT let them imply --remote: that would silently switch the user to
|
|
41
|
+
// API-key (billed) cloud sessions. Require the mode to be stated explicitly.
|
|
42
|
+
const remoteOnlyFlags = remoteOnlyFlagsInUse(flags);
|
|
43
|
+
if (remoteOnlyFlags.length > 0 && !flags.remote) {
|
|
44
|
+
const verb = remoteOnlyFlags.length === 1 ? "requires" : "require";
|
|
45
|
+
fail(`${remoteOnlyFlags.join(" and ")} ${verb} --remote. Try: browse open <url> --remote ${remoteOnlyFlags.join(" ")}`);
|
|
46
|
+
}
|
|
30
47
|
if (flags.cdp) {
|
|
31
48
|
failOnConflictingFlags("--cdp", [
|
|
32
49
|
flags["auto-connect"] ? "--auto-connect" : null,
|
|
@@ -123,6 +140,13 @@ export function targetsCompatible(left, right) {
|
|
|
123
140
|
if (left.kind === "cdp" && right.kind === "cdp") {
|
|
124
141
|
return left.endpoint === right.endpoint && left.targetId === right.targetId;
|
|
125
142
|
}
|
|
143
|
+
if (left.kind === "remote" && right.kind === "remote") {
|
|
144
|
+
// verified/proxies are baked in at session creation, so a re-open that asks
|
|
145
|
+
// for different settings can't reuse the running session — make them sticky
|
|
146
|
+
// like headless, forcing an explicit stop-and-reopen.
|
|
147
|
+
return (Boolean(left.verified) === Boolean(right.verified) &&
|
|
148
|
+
Boolean(left.proxies) === Boolean(right.proxies));
|
|
149
|
+
}
|
|
126
150
|
return true;
|
|
127
151
|
}
|
|
128
152
|
function chromeArgsEqual(left, right) {
|
|
@@ -10,7 +10,14 @@ export function resolveExplicitRemoteTarget() {
|
|
|
10
10
|
export function autoSelectRemoteTarget() {
|
|
11
11
|
return null;
|
|
12
12
|
}
|
|
13
|
-
export function
|
|
13
|
+
export function forwardedEnvKeys() {
|
|
14
|
+
// The local-only build never reaches the cloud, so there is nothing to
|
|
15
|
+
// forward — and no API-key names may appear in this artifact.
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
export async function remoteStagehandOptions() {
|
|
19
|
+
// Accepts the forwarded-env arg structurally (fewer params is
|
|
20
|
+
// assignable) without naming it, keeping this stub key-name-free.
|
|
14
21
|
throw new Error(DISABLED_MESSAGE);
|
|
15
22
|
}
|
|
16
23
|
export function classifyRemoteInitError(error) {
|
|
@@ -1,24 +1,56 @@
|
|
|
1
1
|
import { StatusCodes } from "http-status-codes";
|
|
2
|
+
import { getCliVersion, resolveInstallId, toMetadataValue, } from "../identity.js";
|
|
2
3
|
/**
|
|
3
4
|
* Real Browserbase capability. This is the ONLY module that reads
|
|
4
5
|
* `BROWSERBASE_API_KEY`; it is excluded from `build:local-only` so that
|
|
5
6
|
* local-only artifacts cannot reach Browserbase.
|
|
6
7
|
*/
|
|
7
|
-
export function resolveExplicitRemoteTarget() {
|
|
8
|
-
return {
|
|
8
|
+
export function resolveExplicitRemoteTarget(flags) {
|
|
9
|
+
return {
|
|
10
|
+
kind: "remote",
|
|
11
|
+
...(flags.verified ? { verified: true } : {}),
|
|
12
|
+
...(flags.proxies ? { proxies: true } : {}),
|
|
13
|
+
};
|
|
9
14
|
}
|
|
10
15
|
export function autoSelectRemoteTarget() {
|
|
11
16
|
return process.env.BROWSERBASE_API_KEY ? { kind: "remote" } : null;
|
|
12
17
|
}
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Env vars the client forwards to a running daemon. Only the API key needs
|
|
20
|
+
* forwarding: the Browserbase backend infers the project from the key, so a
|
|
21
|
+
* project id is not required for session creation. (A multi-project key that
|
|
22
|
+
* wants to pin a non-default project via BROWSERBASE_PROJECT_ID is a rare edge
|
|
23
|
+
* case; that still resolves from the daemon's own env, not the forwarded set.)
|
|
24
|
+
*/
|
|
25
|
+
export function forwardedEnvKeys() {
|
|
26
|
+
return ["BROWSERBASE_API_KEY"];
|
|
27
|
+
}
|
|
28
|
+
export async function remoteStagehandOptions(target, forwardedEnv) {
|
|
29
|
+
// Prefer the caller's forwarded key; fall back to the daemon's own spawn-time
|
|
30
|
+
// env (e.g. a daemon that was started with a key). Threading the value here
|
|
31
|
+
// avoids writing the key back into the daemon's `process.env`. The project id
|
|
32
|
+
// is left to Stagehand to resolve (constructor opt → env → inferred from key).
|
|
33
|
+
const apiKey = forwardedEnv?.BROWSERBASE_API_KEY ?? process.env.BROWSERBASE_API_KEY;
|
|
15
34
|
if (!apiKey) {
|
|
16
35
|
throw new Error("Missing BROWSERBASE_API_KEY for remote mode. Pass --local to run a managed local browser (no key needed), or set BROWSERBASE_API_KEY for cloud sessions.");
|
|
17
36
|
}
|
|
37
|
+
// Stamp anonymous attribution onto the session. Resolving the install id is
|
|
38
|
+
// best-effort and never throws; if it can't be resolved we still send
|
|
39
|
+
// browse_cli + cli_version so the session stays attributable to the CLI.
|
|
40
|
+
const userMetadata = {
|
|
41
|
+
browse_cli: "true",
|
|
42
|
+
cli_version: toMetadataValue(getCliVersion()),
|
|
43
|
+
};
|
|
44
|
+
const installId = await resolveInstallId(process.env).catch(() => undefined);
|
|
45
|
+
if (installId) {
|
|
46
|
+
userMetadata.install_id = toMetadataValue(installId);
|
|
47
|
+
}
|
|
18
48
|
return {
|
|
19
49
|
apiKey,
|
|
20
50
|
browserbaseSessionCreateParams: {
|
|
21
|
-
userMetadata
|
|
51
|
+
userMetadata,
|
|
52
|
+
...(target?.proxies ? { proxies: true } : {}),
|
|
53
|
+
...(target?.verified ? { browserSettings: { verified: true } } : {}),
|
|
22
54
|
},
|
|
23
55
|
disableAPI: true,
|
|
24
56
|
disablePino: true,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Stagehand } from "@browserbasehq/stagehand";
|
|
2
2
|
import { emptyRefMaps, resolveSelector as resolveCachedSelector, } from "./commands/selectors.js";
|
|
3
3
|
import { executeDriverCommand } from "./commands/registry.js";
|
|
4
|
+
import { forwardedEnvSignature, } from "./daemon/forwarded-env.js";
|
|
4
5
|
import { DriverError } from "./errors.js";
|
|
5
6
|
import { discoverLocalCdp } from "./local-cdp-discovery.js";
|
|
6
7
|
import { NetworkCapture } from "./network-capture.js";
|
|
@@ -37,6 +38,8 @@ export class DriverSessionManager {
|
|
|
37
38
|
network;
|
|
38
39
|
consecutiveInitFailures = 0;
|
|
39
40
|
context = null;
|
|
41
|
+
lastForwardedEnvSignature = null;
|
|
42
|
+
pendingEnv;
|
|
40
43
|
initFailure = null;
|
|
41
44
|
initPromise = null;
|
|
42
45
|
refMaps = emptyRefMaps();
|
|
@@ -53,6 +56,33 @@ export class DriverSessionManager {
|
|
|
53
56
|
async execute(command, params) {
|
|
54
57
|
return executeDriverCommand(this, command, params);
|
|
55
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Apply env vars forwarded by the client (e.g. an inline or exported API
|
|
61
|
+
* key set after the daemon started). Honoring a late key without a manual
|
|
62
|
+
* restart is the whole point of forwarding.
|
|
63
|
+
*
|
|
64
|
+
* The forwarded env is stashed for the next `init()`, which threads it
|
|
65
|
+
* straight into the Stagehand constructor — never into `process.env` — so the
|
|
66
|
+
* key's only home is the live session. A live, already-initialized session
|
|
67
|
+
* keeps its existing browser (forwarded env only matters at init), so the
|
|
68
|
+
* warm-daemon fast path is untouched. When the forwarded env changes *before*
|
|
69
|
+
* a successful init (the common case: a first key-less `open` failed, then a
|
|
70
|
+
* key is supplied), clear the cached init failure and backoff so the retry
|
|
71
|
+
* runs immediately with the new key instead of replaying the stale
|
|
72
|
+
* missing-key error.
|
|
73
|
+
*/
|
|
74
|
+
applyForwardedEnv(forwardedEnv) {
|
|
75
|
+
// Keep the caller's latest forwarded env available to the next init.
|
|
76
|
+
this.pendingEnv = forwardedEnv;
|
|
77
|
+
const signature = forwardedEnvSignature(forwardedEnv);
|
|
78
|
+
if (signature === this.lastForwardedEnvSignature)
|
|
79
|
+
return;
|
|
80
|
+
this.lastForwardedEnvSignature = signature;
|
|
81
|
+
if (this.stagehand && this.context)
|
|
82
|
+
return;
|
|
83
|
+
this.initFailure = null;
|
|
84
|
+
this.consecutiveInitFailures = 0;
|
|
85
|
+
}
|
|
56
86
|
async activePage() {
|
|
57
87
|
return this.ensurePage();
|
|
58
88
|
}
|
|
@@ -89,6 +119,7 @@ export class DriverSessionManager {
|
|
|
89
119
|
const page = this.activePageIfPresent();
|
|
90
120
|
const pages = await this.pageSummaries();
|
|
91
121
|
return {
|
|
122
|
+
...this.browserbaseIdentity(),
|
|
92
123
|
browserConnected: true,
|
|
93
124
|
initialized: true,
|
|
94
125
|
mode: this.target.kind,
|
|
@@ -101,6 +132,28 @@ export class DriverSessionManager {
|
|
|
101
132
|
url: page?.url(),
|
|
102
133
|
};
|
|
103
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Browserbase session identity (id, dashboard URL, live-view/debug URL) for a
|
|
137
|
+
* live remote session. Lets `status`/`open`/`doctor` reason about the cloud
|
|
138
|
+
* session instead of losing it the way a raw `--cdp` attach does. Empty for
|
|
139
|
+
* non-remote targets or before the driver has initialized.
|
|
140
|
+
*/
|
|
141
|
+
browserbaseIdentity() {
|
|
142
|
+
if (this.target.kind !== "remote" || !this.stagehand)
|
|
143
|
+
return {};
|
|
144
|
+
const { browserbaseSessionID, browserbaseSessionURL, browserbaseDebugURL } = this.stagehand;
|
|
145
|
+
const identity = {};
|
|
146
|
+
if (browserbaseSessionID) {
|
|
147
|
+
identity.browserbaseSessionId = browserbaseSessionID;
|
|
148
|
+
}
|
|
149
|
+
if (browserbaseSessionURL) {
|
|
150
|
+
identity.browserbaseSessionUrl = browserbaseSessionURL;
|
|
151
|
+
}
|
|
152
|
+
if (browserbaseDebugURL) {
|
|
153
|
+
identity.browserbaseDebugUrl = browserbaseDebugURL;
|
|
154
|
+
}
|
|
155
|
+
return identity;
|
|
156
|
+
}
|
|
104
157
|
async close() {
|
|
105
158
|
const stagehand = this.stagehand;
|
|
106
159
|
this.stagehand = null;
|
|
@@ -123,6 +176,7 @@ export class DriverSessionManager {
|
|
|
123
176
|
}
|
|
124
177
|
async openResult(page) {
|
|
125
178
|
return {
|
|
179
|
+
...this.browserbaseIdentity(),
|
|
126
180
|
mode: this.target.kind,
|
|
127
181
|
pages: await this.pageSummaries(),
|
|
128
182
|
selectedTargetId: page.targetId(),
|
|
@@ -262,7 +316,7 @@ export class DriverSessionManager {
|
|
|
262
316
|
}
|
|
263
317
|
async stagehandOptions(target) {
|
|
264
318
|
if (target.kind === "remote") {
|
|
265
|
-
return (await getRemote()).remoteStagehandOptions();
|
|
319
|
+
return await (await getRemote()).remoteStagehandOptions(target, this.pendingEnv);
|
|
266
320
|
}
|
|
267
321
|
if (target.kind === "managed-local") {
|
|
268
322
|
return {
|