From 3985d378a24b738aade765564597124e9d85020c Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Thu, 13 Aug 2026 01:44:57 +0000 Subject: [PATCH 1/4] fix(cli): align Functions with SDK --- .changeset/warm-functions-parity.md | 5 ++ packages/cli/README.md | 4 ++ packages/cli/skills/browse/SKILL.md | 2 +- packages/cli/src/commands/functions/dev.ts | 7 ++ packages/cli/src/commands/functions/invoke.ts | 2 + .../cli/src/commands/functions/publish.ts | 7 ++ packages/cli/src/lib/functions/dev.ts | 46 ++++++++++--- packages/cli/src/lib/functions/init.ts | 19 +++-- packages/cli/src/lib/functions/publish.ts | 20 +++++- packages/cli/src/lib/functions/shared.ts | 32 +++++++++ .../cli/tests/cli-functions-contract.test.ts | 69 ++++++++++++++++--- 11 files changed, 184 insertions(+), 29 deletions(-) create mode 100644 .changeset/warm-functions-parity.md diff --git a/.changeset/warm-functions-parity.md b/.changeset/warm-functions-parity.md new file mode 100644 index 000000000..ece5c36ef --- /dev/null +++ b/.changeset/warm-functions-parity.md @@ -0,0 +1,5 @@ +--- +"browse": patch +--- + +Align `browse functions` with the Browserbase Functions SDK by forwarding explicit project IDs during local development and publishing, matching local invocation context and error payloads, and accepting the SDK's `--api-url` option. diff --git a/packages/cli/README.md b/packages/cli/README.md index 416dc1b3a..f877dacf1 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -256,6 +256,10 @@ browse functions invoke --params '{"url":"https://example.com"}' browse functions invoke --check-status ``` +Set both `BROWSERBASE_API_KEY` and `BROWSERBASE_PROJECT_ID` for `functions dev` and +`functions publish`. `functions invoke` only requires `BROWSERBASE_API_KEY`. You can also pass +`--api-key` and `--project-id` explicitly; `--api-url` is accepted as an alias for `--base-url`. + ## Templates Discover and scaffold ready-to-run Browserbase example projects. diff --git a/packages/cli/skills/browse/SKILL.md b/packages/cli/skills/browse/SKILL.md index 9ddf70137..1126ad960 100644 --- a/packages/cli/skills/browse/SKILL.md +++ b/packages/cli/skills/browse/SKILL.md @@ -277,7 +277,7 @@ browse functions invoke --params '{"url":"https://example.com"}' browse functions invoke --check-status ``` -Functions commands use `BROWSERBASE_API_KEY`. Generated projects import `defineFn` from `@browserbasehq/sdk-functions`. +Functions commands use `BROWSERBASE_API_KEY`; local development and publishing also require `BROWSERBASE_PROJECT_ID`. Generated projects import `defineFn` from `@browserbasehq/sdk-functions`. ## Templates diff --git a/packages/cli/src/commands/functions/dev.ts b/packages/cli/src/commands/functions/dev.ts index 129832a08..36dc71e22 100644 --- a/packages/cli/src/commands/functions/dev.ts +++ b/packages/cli/src/commands/functions/dev.ts @@ -25,6 +25,8 @@ export default class FunctionsDev extends BrowseCommand { helpValue: "", }), "base-url": Flags.string({ + aliases: ["api-url"], + char: "u", description: "Override the Browserbase API base URL.", helpValue: "", }), @@ -38,6 +40,10 @@ export default class FunctionsDev extends BrowseCommand { description: "Port to listen on.", helpValue: "", }), + "project-id": Flags.string({ + description: "Browserbase project ID used for local browser sessions.", + helpValue: "", + }), verbose: Flags.boolean({ description: "Print verbose runtime logs.", }), @@ -51,6 +57,7 @@ export default class FunctionsDev extends BrowseCommand { entrypoint: args.entrypoint, host: flags.host, port: flags.port, + projectId: flags["project-id"], verbose: flags.verbose ?? false, }); } diff --git a/packages/cli/src/commands/functions/invoke.ts b/packages/cli/src/commands/functions/invoke.ts index 3d33a0368..6d809f4e6 100644 --- a/packages/cli/src/commands/functions/invoke.ts +++ b/packages/cli/src/commands/functions/invoke.ts @@ -26,6 +26,8 @@ export default class FunctionsInvoke extends BrowseCommand { helpValue: "", }), "base-url": Flags.string({ + aliases: ["api-url"], + char: "u", description: "Override the Browserbase API base URL.", helpValue: "", }), diff --git a/packages/cli/src/commands/functions/publish.ts b/packages/cli/src/commands/functions/publish.ts index 9c3cd6de5..61a06e4b9 100644 --- a/packages/cli/src/commands/functions/publish.ts +++ b/packages/cli/src/commands/functions/publish.ts @@ -25,12 +25,18 @@ export default class FunctionsPublish extends BrowseCommand { helpValue: "", }), "base-url": Flags.string({ + aliases: ["api-url"], + char: "u", description: "Override the Browserbase API base URL.", helpValue: "", }), "dry-run": Flags.boolean({ description: "Show what would be published without uploading.", }), + "project-id": Flags.string({ + description: "Browserbase project ID to publish into.", + helpValue: "", + }), }; async run(): Promise { @@ -40,6 +46,7 @@ export default class FunctionsPublish extends BrowseCommand { baseUrl: flags["base-url"], dryRun: flags["dry-run"] ?? false, entrypoint: args.entrypoint, + projectId: flags["project-id"], }); } } diff --git a/packages/cli/src/lib/functions/dev.ts b/packages/cli/src/lib/functions/dev.ts index 249f2815d..0286f6013 100644 --- a/packages/cli/src/lib/functions/dev.ts +++ b/packages/cli/src/lib/functions/dev.ts @@ -15,8 +15,8 @@ import { fail } from "../errors.js"; import { functionsRequest, resolveEntrypoint, - resolveFunctionsApiConfig, - type FunctionsApiConfig, + resolveFunctionsProjectConfig, + type FunctionsProjectConfig, } from "./shared.js"; const DEFAULT_RUNTIME_STARTUP_TIMEOUT_MS = 10_000; @@ -27,10 +27,15 @@ export interface StartFunctionsDevServerOptions { entrypoint: string; host: string; port: number; + projectId?: string; verbose: boolean; } interface InvocationContext { + invocation: { + id: string; + region: "local"; + }; session: { id: string; connectUrl: string; @@ -120,7 +125,13 @@ class InvocationBridge { sendJson( this.invokeConnection.response, 500, - { error: payload }, + { + error: { + message: payload.errorMessage, + stackTrace: payload.stackTrace, + type: payload.errorType, + }, + }, this.invokeConnection.corsHeaders, ); try { @@ -154,7 +165,7 @@ class InvocationBridge { "content-type": "application/json", "Lambda-Runtime-Aws-Request-Id": requestId, "Lambda-Runtime-Deadline-Ms": String(Date.now() + 300_000), - "Lambda-Runtime-Invoked-Function-Arn": `arn:aws:lambda:local:function:${functionName}`, + "Lambda-Runtime-Invoked-Function-Arn": `arn:aws:lambda:us-east-1:000000000000:function:${functionName}`, }); this.nextConnection.response.end( JSON.stringify({ @@ -186,7 +197,7 @@ class InvocationBridge { } class BrowserSessionManager { - constructor(private readonly config: FunctionsApiConfig) {} + constructor(private readonly config: FunctionsProjectConfig) {} async createSession( sessionConfig: Record = {}, @@ -196,7 +207,10 @@ class BrowserSessionManager { headers: { "content-type": "application/json", }, - body: JSON.stringify(sessionConfig), + body: JSON.stringify({ + projectId: this.config.projectId, + ...sessionConfig, + }), }); const session = (await response.json()) as { id?: string; @@ -219,7 +233,10 @@ class BrowserSessionManager { headers: { "content-type": "application/json", }, - body: JSON.stringify({ status: "REQUEST_RELEASE" }), + body: JSON.stringify({ + projectId: this.config.projectId, + status: "REQUEST_RELEASE", + }), }); } } @@ -359,7 +376,7 @@ export async function startFunctionsDevServer( fail("Port must be an integer between 1 and 65535."); } - const config = resolveFunctionsApiConfig(options); + const config = resolveFunctionsProjectConfig(options); const runtimeApi = `${options.host}:${options.port}`; const bridge = new InvocationBridge(); const sessionManager = new BrowserSessionManager(config); @@ -587,7 +604,13 @@ async function routeRequest( const accepted = bridge.triggerInvocation( functionName, params, - { session }, + { + invocation: { + id: randomUUID(), + region: "local", + }, + session, + }, corsHeaders, response, ); @@ -664,11 +687,12 @@ async function routeRequest( ); return; } - const completed = await bridge.completeWithError(requestId, { + const runtimeError = { errorMessage: payload?.errorMessage || "Unknown runtime error", errorType: payload?.errorType || "RuntimeError", stackTrace: Array.isArray(payload?.stackTrace) ? payload.stackTrace : [], - }); + }; + const completed = await bridge.completeWithError(requestId, runtimeError); sendJson( response, completed ? 202 : 400, diff --git a/packages/cli/src/lib/functions/init.ts b/packages/cli/src/lib/functions/init.ts index 2f5607642..17a7e3aee 100644 --- a/packages/cli/src/lib/functions/init.ts +++ b/packages/cli/src/lib/functions/init.ts @@ -6,9 +6,10 @@ import { join, resolve } from "node:path"; import { fail } from "../errors.js"; const envTemplate = `# Browserbase Configuration -# Get your API key from https://browserbase.com/settings +# Get your API key and project ID from https://browserbase.com/settings BROWSERBASE_API_KEY=your_api_key_here +BROWSERBASE_PROJECT_ID=your_project_id_here `; const gitignoreTemplate = `node_modules/ @@ -68,7 +69,7 @@ export async function initFunctionsProject({ ); } - ensureCommand(packageManager); + const packageManagerVersion = ensureCommand(packageManager); const projectRoot = resolve(projectName); if (existsSync(projectRoot)) { @@ -79,7 +80,9 @@ export async function initFunctionsProject({ const packageJson = { name: projectName, + version: "1.0.0", private: true, + packageManager: `${packageManager}@${packageManagerVersion}`, type: "module", scripts: { dev: "browse functions dev index.ts", @@ -102,7 +105,7 @@ export async function initFunctionsProject({ runPackageManager( packageManager, - [...install, "@browserbasehq/sdk-functions", "playwright-core"], + [...install, "@browserbasehq/sdk-functions", "playwright-core", "zod"], projectRoot, ); runPackageManager( @@ -126,7 +129,7 @@ export async function initFunctionsProject({ projectRoot, nextSteps: [ `cd ${projectName}`, - "Edit .env with your Browserbase API key", + "Edit .env with your Browserbase API key and project ID", packageManager === "pnpm" ? "pnpm dev" : "npm run dev", packageManager === "pnpm" ? "pnpm run deploy" : "npm run deploy", ], @@ -137,11 +140,15 @@ export async function initFunctionsProject({ ); } -function ensureCommand(command: string): void { - const result = spawnSync(command, ["--version"], { stdio: "ignore" }); +function ensureCommand(command: string): string { + const result = spawnSync(command, ["--version"], { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }); if (result.error || result.status !== 0) { fail(`${command} is required but was not found on PATH.`); } + return result.stdout.trim(); } function runPackageManager( diff --git a/packages/cli/src/lib/functions/publish.ts b/packages/cli/src/lib/functions/publish.ts index a71416f22..56221fe69 100644 --- a/packages/cli/src/lib/functions/publish.ts +++ b/packages/cli/src/lib/functions/publish.ts @@ -21,7 +21,7 @@ import { functionsRequest, pollUntil, resolveEntrypoint, - resolveFunctionsApiConfig, + resolveFunctionsProjectConfig, } from "./shared.js"; export interface PublishFunctionOptions { @@ -29,6 +29,7 @@ export interface PublishFunctionOptions { baseUrl?: string; dryRun: boolean; entrypoint: string; + projectId?: string; } interface BuildUploadResponse { @@ -67,11 +68,13 @@ const defaultIgnorePatterns = [ ".browserbase/", ]; +const maxArchiveSizeBytes = 50 * 1024 * 1024; + export async function publishFunction( options: PublishFunctionOptions, ): Promise { const entrypoint = await resolveEntrypoint(options.entrypoint); - const config = resolveFunctionsApiConfig(options); + const config = resolveFunctionsProjectConfig(options); const entrypointPath = relative(process.cwd(), entrypoint); if (options.dryRun) { @@ -84,6 +87,7 @@ export async function publishFunction( dryRun: true, entrypoint: entrypointPath, files: entries, + projectId: config.projectId, }, null, 2, @@ -94,8 +98,18 @@ export async function publishFunction( const { archivePath } = await createArchive(process.cwd()); try { + const archiveStats = await stat(archivePath); + if (archiveStats.size > maxArchiveSizeBytes) { + fail( + `Functions archive is ${(archiveStats.size / 1024 / 1024).toFixed(2)} MB; the maximum is 50 MB. Add files to .gitignore to reduce its size.`, + ); + } + const formData = new FormData(); - formData.append("metadata", JSON.stringify({ entrypoint: entrypointPath })); + formData.append( + "metadata", + JSON.stringify({ entrypoint: entrypointPath, projectId: config.projectId }), + ); formData.append( "archive", new Blob([await readFile(archivePath)], { type: "application/gzip" }), diff --git a/packages/cli/src/lib/functions/shared.ts b/packages/cli/src/lib/functions/shared.ts index 1be7e9bcd..3f6c04a01 100644 --- a/packages/cli/src/lib/functions/shared.ts +++ b/packages/cli/src/lib/functions/shared.ts @@ -16,6 +16,10 @@ export interface FunctionsApiConfig { baseUrl: string; } +export interface FunctionsProjectConfig extends FunctionsApiConfig { + projectId: string; +} + export interface PollOptions { done: (value: T) => boolean; intervalMs?: number; @@ -36,6 +40,34 @@ export function resolveFunctionsApiConfig(args: { }; } +export function resolveFunctionsProjectConfig(args: { + apiKey?: string; + baseUrl?: string; + projectId?: string; +}): FunctionsProjectConfig { + const apiConfig = resolveFunctionsApiConfig(args); + const projectId = args.projectId ?? process.env.BROWSERBASE_PROJECT_ID; + if (!projectId) { + fail( + [ + "Missing Browserbase project ID. Functions dev and publish need an explicit project.", + "Set BROWSERBASE_PROJECT_ID or pass --project-id.", + "Find your project ID at https://browserbase.com/settings.", + ].join("\n"), + 1, + { + resultCode: "missing_project_id", + requestHadHttpResponse: false, + }, + ); + } + + return { + ...apiConfig, + projectId, + }; +} + export async function functionsRequest( config: FunctionsApiConfig, path: string, diff --git a/packages/cli/tests/cli-functions-contract.test.ts b/packages/cli/tests/cli-functions-contract.test.ts index 57ab7e69d..d5ee309c4 100644 --- a/packages/cli/tests/cli-functions-contract.test.ts +++ b/packages/cli/tests/cli-functions-contract.test.ts @@ -85,7 +85,9 @@ describe("functions API contracts", () => { "index.ts", "--api-key", "test-key", - "--base-url", + "--project-id", + "test-project", + "--api-url", baseUrl, ], { cwd }, @@ -101,6 +103,7 @@ describe("functions API contracts", () => { "multipart/form-data", ); expect(requests[0]?.bodyText).toContain('"entrypoint":"index.ts"'); + expect(requests[0]?.bodyText).toContain('"projectId":"test-project"'); expectRequest( requests[1], "GET", @@ -133,6 +136,8 @@ describe("functions API contracts", () => { "--dry-run", "--api-key", "test-key", + "--project-id", + "test-project", ], { cwd, @@ -147,9 +152,11 @@ describe("functions API contracts", () => { dryRun: boolean; entrypoint: string; files: string[]; + projectId: string; }; expect(output.dryRun).toBe(true); expect(output.entrypoint).toBe("index.ts"); + expect(output.projectId).toBe("test-project"); expect(output.files).toContain("index.ts"); expect(output.files).toContain("package.json"); expect(output.files.some((file) => file.startsWith(".browserbase/"))).toBe( @@ -183,6 +190,8 @@ describe("functions API contracts", () => { "index.ts", "--api-key", "test-key", + "--project-id", + "test-project", "--base-url", baseUrl, ], @@ -198,6 +207,22 @@ describe("functions API contracts", () => { ); }); + it("requires a project ID before publishing", async () => { + const cwd = await createFunctionFixture("functions-missing-project-"); + const result = await runCli( + ["functions", "publish", "index.ts", "--dry-run", "--api-key", "test-key"], + { + cwd, + env: { + BROWSERBASE_PROJECT_ID: "", + }, + }, + ); + + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain("Missing Browserbase project ID"); + }); + it("invokes a deployed Function and polls invocation status", async () => { await withServer( async (request, response) => { @@ -237,7 +262,7 @@ describe("functions API contracts", () => { '{"url":"https://example.com"}', "--api-key", "test-key", - "--base-url", + "--api-url", baseUrl, ]); @@ -338,9 +363,17 @@ describe("functions scaffolding and local dev", () => { expect(entrypoint).toContain( 'import { defineFn } from "@browserbasehq/sdk-functions";', ); - expect( - await readFile(join(cwd, "demo-function", ".env"), "utf8"), - ).toContain("BROWSERBASE_API_KEY="); + const packageJson = JSON.parse( + await readFile(join(cwd, "demo-function", "package.json"), "utf8"), + ) as { + packageManager?: string; + version?: string; + }; + expect(packageJson.packageManager).toBe("pnpm@10.0.0"); + expect(packageJson.version).toBe("1.0.0"); + const envFile = await readFile(join(cwd, "demo-function", ".env"), "utf8"); + expect(envFile).toContain("BROWSERBASE_API_KEY="); + expect(envFile).toContain("BROWSERBASE_PROJECT_ID="); }); it("runs a local dev server and invokes a function", async () => { @@ -383,6 +416,8 @@ describe("functions scaffolding and local dev", () => { String(port), "--api-key", "test-key", + "--project-id", + "test-project", "--base-url", baseUrl, ], @@ -434,12 +469,21 @@ describe("functions scaffolding and local dev", () => { await expect(invokeResponse.json()).resolves.toMatchObject({ ok: true, params: { answer: 42 }, + invocation: { + id: expect.any(String), + region: "local", + }, sessionId: "sess_123", }); await waitForRequests(requests, 2); expectRequest(requests[0], "POST", "/v1/sessions", "test-key"); + expect(requests[0]?.jsonBody).toMatchObject({ projectId: "test-project" }); expectRequest(requests[1], "POST", "/v1/sessions/sess_123", "test-key"); + expect(requests[1]?.jsonBody).toMatchObject({ + projectId: "test-project", + status: "REQUEST_RELEASE", + }); }, ); }, 30_000); @@ -480,6 +524,7 @@ describe("functions scaffolding and local dev", () => { cwd, env: { ...process.env, + BROWSERBASE_PROJECT_ID: "test-project", NODE_ENV: "test", }, stdio: ["ignore", "pipe", "pipe"], @@ -557,6 +602,7 @@ describe("functions scaffolding and local dev", () => { cwd, env: { ...process.env, + BROWSERBASE_PROJECT_ID: "test-project", BROWSERBASE_FUNCTIONS_DEV_STARTUP_TIMEOUT_MS: "0", NODE_ENV: "test", }, @@ -605,6 +651,7 @@ describe("functions scaffolding and local dev", () => { cwd, env: { ...process.env, + BROWSERBASE_PROJECT_ID: "test-project", NODE_ENV: "test", }, stdio: ["ignore", "pipe", "pipe"], @@ -680,6 +727,7 @@ describe("functions scaffolding and local dev", () => { cwd, env: { ...process.env, + BROWSERBASE_PROJECT_ID: "test-project", NODE_ENV: "test", }, stdio: ["ignore", "pipe", "pipe"], @@ -758,6 +806,7 @@ describe("functions scaffolding and local dev", () => { cwd, env: { ...process.env, + BROWSERBASE_PROJECT_ID: "test-project", NODE_ENV: "test", }, stdio: ["ignore", "pipe", "pipe"], @@ -772,9 +821,10 @@ describe("functions scaffolding and local dev", () => { expect(first.headers.get("access-control-allow-origin")).toBeNull(); await expect(first.json()).resolves.toMatchObject({ error: { - errorMessage: expect.stringContaining( + message: expect.stringContaining( "Invalid runtime response payload", ), + type: "RuntimeResponseError", }, }); await waitForFileText(runtimeStatusLog, "400\n"); @@ -783,9 +833,10 @@ describe("functions scaffolding and local dev", () => { expect(second.status).toBe(500); await expect(second.json()).resolves.toMatchObject({ error: { - errorMessage: expect.stringContaining( + message: expect.stringContaining( "Invalid runtime response payload", ), + type: "RuntimeResponseError", }, }); await waitForFileText(runtimeStatusLog, "400\n400\n"); @@ -821,6 +872,7 @@ describe("functions scaffolding and local dev", () => { cwd, env: { ...process.env, + BROWSERBASE_PROJECT_ID: "test-project", NODE_ENV: "test", }, stdio: ["ignore", "pipe", "pipe"], @@ -863,7 +915,7 @@ async function createTempDir(prefix: string): Promise { async function createFakePackageManagerBin( name = "pnpm", - contents = "#!/bin/sh\nexit 0\n", + contents = "#!/bin/sh\necho 10.0.0\n", ): Promise { const directory = await createTempDir("functions-fake-bin-"); const scriptPath = join(directory, name); @@ -916,6 +968,7 @@ while (true) { headers: { "content-type": "application/json" }, body: JSON.stringify({ ok: true, + invocation: event.context.invocation, params: event.params, sessionId: event.context.session.id, }), From 79ac2783d24c5bab6ae679e561e6e4b81a678ea1 Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Thu, 13 Aug 2026 05:09:35 +0000 Subject: [PATCH 2/4] fix(cli): infer Functions project from API key --- .changeset/warm-functions-parity.md | 2 +- packages/cli/README.md | 6 ++--- packages/cli/skills/browse/SKILL.md | 2 +- packages/cli/src/lib/functions/init.ts | 5 ++--- packages/cli/src/lib/functions/publish.ts | 5 ++++- packages/cli/src/lib/functions/shared.ts | 22 ++----------------- .../cli/tests/cli-functions-contract.test.ts | 21 +++++++++++++----- 7 files changed, 28 insertions(+), 35 deletions(-) diff --git a/.changeset/warm-functions-parity.md b/.changeset/warm-functions-parity.md index ece5c36ef..a29162f9d 100644 --- a/.changeset/warm-functions-parity.md +++ b/.changeset/warm-functions-parity.md @@ -2,4 +2,4 @@ "browse": patch --- -Align `browse functions` with the Browserbase Functions SDK by forwarding explicit project IDs during local development and publishing, matching local invocation context and error payloads, and accepting the SDK's `--api-url` option. +Align `browse functions` with the Browserbase Functions SDK by supporting optional project overrides, matching local invocation context and error payloads, and accepting the SDK's `--api-url` option. diff --git a/packages/cli/README.md b/packages/cli/README.md index f877dacf1..953a8de3e 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -256,9 +256,9 @@ browse functions invoke --params '{"url":"https://example.com"}' browse functions invoke --check-status ``` -Set both `BROWSERBASE_API_KEY` and `BROWSERBASE_PROJECT_ID` for `functions dev` and -`functions publish`. `functions invoke` only requires `BROWSERBASE_API_KEY`. You can also pass -`--api-key` and `--project-id` explicitly; `--api-url` is accepted as an alias for `--base-url`. +Set `BROWSERBASE_API_KEY` for Functions commands. Browserbase infers the project +from the key; `--project-id` remains available as an explicit override, and +`--api-url` is accepted as an alias for `--base-url`. ## Templates diff --git a/packages/cli/skills/browse/SKILL.md b/packages/cli/skills/browse/SKILL.md index 1126ad960..2f7d05771 100644 --- a/packages/cli/skills/browse/SKILL.md +++ b/packages/cli/skills/browse/SKILL.md @@ -277,7 +277,7 @@ browse functions invoke --params '{"url":"https://example.com"}' browse functions invoke --check-status ``` -Functions commands use `BROWSERBASE_API_KEY`; local development and publishing also require `BROWSERBASE_PROJECT_ID`. Generated projects import `defineFn` from `@browserbasehq/sdk-functions`. +Functions commands use `BROWSERBASE_API_KEY`, and Browserbase infers the project from the key. Generated projects import `defineFn` from `@browserbasehq/sdk-functions`. ## Templates diff --git a/packages/cli/src/lib/functions/init.ts b/packages/cli/src/lib/functions/init.ts index 17a7e3aee..bd368be8b 100644 --- a/packages/cli/src/lib/functions/init.ts +++ b/packages/cli/src/lib/functions/init.ts @@ -6,10 +6,9 @@ import { join, resolve } from "node:path"; import { fail } from "../errors.js"; const envTemplate = `# Browserbase Configuration -# Get your API key and project ID from https://browserbase.com/settings +# Get your API key from https://browserbase.com/settings BROWSERBASE_API_KEY=your_api_key_here -BROWSERBASE_PROJECT_ID=your_project_id_here `; const gitignoreTemplate = `node_modules/ @@ -129,7 +128,7 @@ export async function initFunctionsProject({ projectRoot, nextSteps: [ `cd ${projectName}`, - "Edit .env with your Browserbase API key and project ID", + "Edit .env with your Browserbase API key", packageManager === "pnpm" ? "pnpm dev" : "npm run dev", packageManager === "pnpm" ? "pnpm run deploy" : "npm run deploy", ], diff --git a/packages/cli/src/lib/functions/publish.ts b/packages/cli/src/lib/functions/publish.ts index 56221fe69..5953ff5da 100644 --- a/packages/cli/src/lib/functions/publish.ts +++ b/packages/cli/src/lib/functions/publish.ts @@ -108,7 +108,10 @@ export async function publishFunction( const formData = new FormData(); formData.append( "metadata", - JSON.stringify({ entrypoint: entrypointPath, projectId: config.projectId }), + JSON.stringify({ + entrypoint: entrypointPath, + projectId: config.projectId, + }), ); formData.append( "archive", diff --git a/packages/cli/src/lib/functions/shared.ts b/packages/cli/src/lib/functions/shared.ts index 3f6c04a01..6803f1620 100644 --- a/packages/cli/src/lib/functions/shared.ts +++ b/packages/cli/src/lib/functions/shared.ts @@ -17,7 +17,7 @@ export interface FunctionsApiConfig { } export interface FunctionsProjectConfig extends FunctionsApiConfig { - projectId: string; + projectId?: string; } export interface PollOptions { @@ -47,25 +47,7 @@ export function resolveFunctionsProjectConfig(args: { }): FunctionsProjectConfig { const apiConfig = resolveFunctionsApiConfig(args); const projectId = args.projectId ?? process.env.BROWSERBASE_PROJECT_ID; - if (!projectId) { - fail( - [ - "Missing Browserbase project ID. Functions dev and publish need an explicit project.", - "Set BROWSERBASE_PROJECT_ID or pass --project-id.", - "Find your project ID at https://browserbase.com/settings.", - ].join("\n"), - 1, - { - resultCode: "missing_project_id", - requestHadHttpResponse: false, - }, - ); - } - - return { - ...apiConfig, - projectId, - }; + return projectId ? { ...apiConfig, projectId } : apiConfig; } export async function functionsRequest( diff --git a/packages/cli/tests/cli-functions-contract.test.ts b/packages/cli/tests/cli-functions-contract.test.ts index d5ee309c4..e22d41d6f 100644 --- a/packages/cli/tests/cli-functions-contract.test.ts +++ b/packages/cli/tests/cli-functions-contract.test.ts @@ -207,10 +207,17 @@ describe("functions API contracts", () => { ); }); - it("requires a project ID before publishing", async () => { + it("infers the project when no project ID is provided", async () => { const cwd = await createFunctionFixture("functions-missing-project-"); const result = await runCli( - ["functions", "publish", "index.ts", "--dry-run", "--api-key", "test-key"], + [ + "functions", + "publish", + "index.ts", + "--dry-run", + "--api-key", + "test-key", + ], { cwd, env: { @@ -219,8 +226,8 @@ describe("functions API contracts", () => { }, ); - expect(result.exitCode).toBe(1); - expect(result.stderr).toContain("Missing Browserbase project ID"); + expect(result.exitCode).toBe(0); + expect(JSON.parse(result.stdout)).not.toHaveProperty("projectId"); }); it("invokes a deployed Function and polls invocation status", async () => { @@ -373,7 +380,7 @@ describe("functions scaffolding and local dev", () => { expect(packageJson.version).toBe("1.0.0"); const envFile = await readFile(join(cwd, "demo-function", ".env"), "utf8"); expect(envFile).toContain("BROWSERBASE_API_KEY="); - expect(envFile).toContain("BROWSERBASE_PROJECT_ID="); + expect(envFile).not.toContain("BROWSERBASE_PROJECT_ID="); }); it("runs a local dev server and invokes a function", async () => { @@ -478,7 +485,9 @@ describe("functions scaffolding and local dev", () => { await waitForRequests(requests, 2); expectRequest(requests[0], "POST", "/v1/sessions", "test-key"); - expect(requests[0]?.jsonBody).toMatchObject({ projectId: "test-project" }); + expect(requests[0]?.jsonBody).toMatchObject({ + projectId: "test-project", + }); expectRequest(requests[1], "POST", "/v1/sessions/sess_123", "test-key"); expect(requests[1]?.jsonBody).toMatchObject({ projectId: "test-project", From f331d36c3e6964b42cc6c152e80455e92e0e35dc Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Thu, 13 Aug 2026 16:07:23 +0000 Subject: [PATCH 3/4] fix: address Cubic review comments --- packages/cli/src/lib/functions/dev.ts | 9 ++--- .../cli/tests/cli-functions-contract.test.ts | 34 +++++++++++++++++-- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/lib/functions/dev.ts b/packages/cli/src/lib/functions/dev.ts index 0286f6013..98fc4507e 100644 --- a/packages/cli/src/lib/functions/dev.ts +++ b/packages/cli/src/lib/functions/dev.ts @@ -202,15 +202,16 @@ class BrowserSessionManager { async createSession( sessionConfig: Record = {}, ): Promise { + const body: Record = { ...sessionConfig }; + if (this.config.projectId !== undefined) { + body.projectId = this.config.projectId; + } const response = await functionsRequest(this.config, "/v1/sessions", { method: "POST", headers: { "content-type": "application/json", }, - body: JSON.stringify({ - projectId: this.config.projectId, - ...sessionConfig, - }), + body: JSON.stringify(body), }); const session = (await response.json()) as { id?: string; diff --git a/packages/cli/tests/cli-functions-contract.test.ts b/packages/cli/tests/cli-functions-contract.test.ts index e22d41d6f..2da97c755 100644 --- a/packages/cli/tests/cli-functions-contract.test.ts +++ b/packages/cli/tests/cli-functions-contract.test.ts @@ -7,6 +7,7 @@ import { writeFile, } from "node:fs/promises"; import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; +import { randomBytes } from "node:crypto"; import { createServer } from "node:net"; import { dirname, join, resolve } from "node:path"; import { tmpdir } from "node:os"; @@ -207,6 +208,27 @@ describe("functions API contracts", () => { ); }); + it("rejects publish archives larger than 50 MB", async () => { + const cwd = await createFunctionFixture("functions-publish-oversized-"); + await writeFile(join(cwd, "payload.bin"), randomBytes(51 * 1024 * 1024)); + + const result = await runCli( + [ + "functions", + "publish", + "index.ts", + "--api-key", + "test-key", + "--project-id", + "test-project", + ], + { cwd }, + ); + + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain("the maximum is 50 MB"); + }, 30_000); + it("infers the project when no project ID is provided", async () => { const cwd = await createFunctionFixture("functions-missing-project-"); const result = await runCli( @@ -386,7 +408,9 @@ describe("functions scaffolding and local dev", () => { it("runs a local dev server and invokes a function", async () => { const cwd = await createTempDir("functions-dev-"); const port = await getFreePort(); - await writeRuntimeEntrypoint(cwd); + await writeRuntimeEntrypoint(cwd, { + sessionConfig: { projectId: "manifest-project" }, + }); await withServer( async (request, response) => { @@ -935,7 +959,11 @@ async function createFakePackageManagerBin( async function writeRuntimeEntrypoint( cwd: string, - options: { malformedResponse?: boolean; runtimeStatusLog?: string } = {}, + options: { + malformedResponse?: boolean; + runtimeStatusLog?: string; + sessionConfig?: Record; + } = {}, ): Promise { await writeFile( join(cwd, "runtime-entry.mjs"), @@ -947,7 +975,7 @@ const manifestsDir = join(process.cwd(), ".browserbase", "functions", "manifests mkdirSync(manifestsDir, { recursive: true }); writeFileSync(join(manifestsDir, "test-function.json"), JSON.stringify({ name: "test-function", - config: {}, + config: { sessionConfig: ${JSON.stringify(options.sessionConfig ?? {})} }, }, null, 2)); const runtimeApi = process.env.AWS_LAMBDA_RUNTIME_API; From bad9aa9d5bd5be9a9f5945d0333e9140b39a82bc Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Thu, 13 Aug 2026 16:14:55 +0000 Subject: [PATCH 4/4] test: cover Functions project inference --- .../cli/tests/cli-functions-contract.test.ts | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/packages/cli/tests/cli-functions-contract.test.ts b/packages/cli/tests/cli-functions-contract.test.ts index 2da97c755..009d14ac3 100644 --- a/packages/cli/tests/cli-functions-contract.test.ts +++ b/packages/cli/tests/cli-functions-contract.test.ts @@ -211,6 +211,7 @@ describe("functions API contracts", () => { it("rejects publish archives larger than 50 MB", async () => { const cwd = await createFunctionFixture("functions-publish-oversized-"); await writeFile(join(cwd, "payload.bin"), randomBytes(51 * 1024 * 1024)); + await writeFile(join(cwd, "package-lock.json"), "{}\n"); const result = await runCli( [ @@ -521,6 +522,81 @@ describe("functions scaffolding and local dev", () => { ); }, 30_000); + it("uses the manifest session project when no CLI override is set", async () => { + const cwd = await createTempDir("functions-dev-manifest-project-"); + const port = await getFreePort(); + await writeRuntimeEntrypoint(cwd, { + sessionConfig: { projectId: "manifest-project" }, + }); + + await withServer( + async (request, response) => { + if (request.method === "POST" && request.path === "/v1/sessions") { + jsonResponse(response, 200, { + connectUrl: "ws://example.test/devtools", + id: "sess_manifest", + }); + return; + } + + if ( + request.method === "POST" && + request.path === "/v1/sessions/sess_manifest" + ) { + jsonResponse(response, 200, { + id: "sess_manifest", + status: "REQUEST_RELEASE", + }); + return; + } + + jsonResponse(response, 404, { error: "not found" }); + }, + async ({ baseUrl, requests }) => { + const child = spawn( + process.execPath, + [ + join(repoRoot, "bin/run.js"), + "functions", + "dev", + "runtime-entry.mjs", + "--port", + String(port), + "--api-key", + "test-key", + "--base-url", + baseUrl, + ], + { + cwd, + env: { + ...process.env, + BROWSERBASE_PROJECT_ID: "", + NODE_ENV: "test", + }, + stdio: ["ignore", "pipe", "pipe"], + }, + ); + cleanupProcesses.push(child); + + await waitForStdout(child, '"ok": true'); + const invokeResponse = await invokeLocalFunction(port, {}); + expect(invokeResponse.status).toBe(200); + await expect(invokeResponse.json()).resolves.toMatchObject({ + sessionId: "sess_manifest", + }); + + await waitForRequests(requests, 2); + expect(requests[0]?.jsonBody).toMatchObject({ + projectId: "manifest-project", + }); + expect(requests[1]?.jsonBody).toEqual({ + status: "REQUEST_RELEASE", + }); + }, + ); + }, 30_000); + it("blocks browser origins outside loopback before creating a session", async () => { const cwd = await createTempDir("functions-dev-cors-"); const port = await getFreePort();