import Browserbase from "@browserbasehq/sdk" import * as z from "zod" import { BROWSERBASE_JSON_OBJECT_SCHEMA } from "./schemas" const BROWSERBASE_API_ORIGIN = "https://api.browserbase.com" const BROWSERBASE_SECRET_SCHEMA = z.object({ apiKey: z.string().min(1) }) interface BrowserbaseRequestOptions { body?: Record method?: "DELETE" | "GET" | "PATCH" | "POST" query?: Record responseSchema: TSchema } /** Error returned by the Browserbase REST API. */ export class BrowserbaseApiError extends Error { readonly status: number /** * Creates a Browserbase API error. * * @param status - HTTP response status. * @param message - Provider error message. */ constructor(status: number, message: string) { super(message) this.name = "BrowserbaseApiError" this.status = status } } /** * Creates the official Browserbase client for an integration account. * * @param secret - Connected account secret. */ export function getBrowserbaseClient(secret: Record) { const { apiKey } = BROWSERBASE_SECRET_SCHEMA.parse(secret) return new Browserbase({ apiKey, fetch: globalThis.fetch, maxRetries: 0 }) } /** * Calls a Browserbase endpoint not yet exposed by its official SDK. * * @param secret - Connected account secret. * @param path - API path. * @param options - Request and response contract. */ export async function requestBrowserbase( secret: Record, path: string, options: BrowserbaseRequestOptions, ): Promise> { const { apiKey } = BROWSERBASE_SECRET_SCHEMA.parse(secret) const url = new URL(path, BROWSERBASE_API_ORIGIN) for (const [name, value] of Object.entries(options.query ?? {})) { if (value !== undefined) url.searchParams.set(name, String(value)) } const response = await fetch(url, { ...(options.body && { body: JSON.stringify(options.body), headers: { Accept: "application/json", "Content-Type": "application/json", "X-BB-API-Key": apiKey, }, }), ...(!options.body && { headers: { Accept: "application/json", "X-BB-API-Key": apiKey }, }), method: options.method ?? "GET", }) if (!response.ok) throw await toBrowserbaseApiError(response) return options.responseSchema.parse(await response.json()) } /** * Calls a Browserbase endpoint that returns text. * * @param secret - Connected account secret. * @param path - API path. */ export async function requestBrowserbaseText( secret: Record, path: string, ) { const { apiKey } = BROWSERBASE_SECRET_SCHEMA.parse(secret) const response = await fetch(new URL(path, BROWSERBASE_API_ORIGIN), { headers: { "X-BB-API-Key": apiKey }, }) if (!response.ok) throw await toBrowserbaseApiError(response) return await response.text() } /** * Calls a Browserbase endpoint that returns file bytes. * * @param secret - Connected account secret. * @param path - API path. */ export async function requestBrowserbaseBlob( secret: Record, path: string, ) { const { apiKey } = BROWSERBASE_SECRET_SCHEMA.parse(secret) const response = await fetch(new URL(path, BROWSERBASE_API_ORIGIN), { headers: { Accept: "application/octet-stream", "X-BB-API-Key": apiKey, }, }) if (!response.ok) throw await toBrowserbaseApiError(response) return await response.blob() } /** * Sends a Browserbase request with no response body. * * @param secret - Connected account secret. * @param path - API path. * @param method - HTTP mutation method. */ export async function requestBrowserbaseEmpty( secret: Record, path: string, method: "DELETE" | "POST", ) { const { apiKey } = BROWSERBASE_SECRET_SCHEMA.parse(secret) const response = await fetch(new URL(path, BROWSERBASE_API_ORIGIN), { headers: { "X-BB-API-Key": apiKey }, method, }) if (!response.ok) throw await toBrowserbaseApiError(response) } /** * Converts an unsuccessful response into a stable provider error. * * @param response - Unsuccessful provider response. */ async function toBrowserbaseApiError(response: Response) { const fallback = `Browserbase request failed (${response.status}).` const text = await response.text() let json: unknown try { json = JSON.parse(text) } catch { return new BrowserbaseApiError(response.status, text || fallback) } const body = BROWSERBASE_JSON_OBJECT_SCHEMA.safeParse(json) return new BrowserbaseApiError( response.status, body.success && typeof body.data.message === "string" ? body.data.message : fallback, ) }