diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4425e63..328f08f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,12 +7,17 @@ on: - 'integrated/**' - 'stl-preview-head/**' - 'stl-preview-base/**' + pull_request: + branches-ignore: + - 'stl-preview-head/**' + - 'stl-preview-base/**' jobs: lint: timeout-minutes: 10 name: lint runs-on: ${{ github.repository == 'stainless-sdks/browserbase-node' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@v4 @@ -31,6 +36,7 @@ jobs: timeout-minutes: 5 name: build runs-on: ${{ github.repository == 'stainless-sdks/browserbase-node' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork permissions: contents: read id-token: write @@ -66,6 +72,7 @@ jobs: timeout-minutes: 10 name: test runs-on: ${{ github.repository == 'stainless-sdks/browserbase-node' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@v4 diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 69e82f1..6ed9c80 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.6.0" + ".": "2.7.0" } diff --git a/.stats.yml b/.stats.yml index 38c95a8..5b4609f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-e2ed1b5267eeff92982918505349017b9155da2c7ab948787ab11cf9068af1b8.yml -openapi_spec_hash: 6639c21dccb52ca610cae833227a9791 -config_hash: 74882e23a455dece33e43a27e67f0fbb +openapi_spec_hash: 77cba4a3c422b7378ecc7d57d84ff0b1 +config_hash: b3ca4ec5b02e5333af51ebc2e9fdef1b diff --git a/CHANGELOG.md b/CHANGELOG.md index e085bb8..0fa7589 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,35 @@ # Changelog +## 2.7.0 (2025-06-30) + +Full Changelog: [v2.6.0...v2.7.0](https://github.com/browserbase/sdk-node/compare/v2.6.0...v2.7.0) + +### Features + +* **client:** add support for endpoint-specific base URLs ([0c69436](https://github.com/browserbase/sdk-node/commit/0c69436d731785b207af5f979e9eb5ae1b975957)) + + +### Bug Fixes + +* **ci:** release-doctor — report correct token name ([8e59beb](https://github.com/browserbase/sdk-node/commit/8e59beb314e8c07bd14b471121e28ec941168190)) +* publish script — handle NPM errors correctly ([87c245e](https://github.com/browserbase/sdk-node/commit/87c245e2048dad1586d97fae8c4622c6e2780365)) + + +### Chores + +* **ci:** enable for pull requests ([132825d](https://github.com/browserbase/sdk-node/commit/132825d5c65ba9aa1c47afe9b8a7b14885129598)) +* **ci:** only run for pushes and fork pull requests ([a166050](https://github.com/browserbase/sdk-node/commit/a166050ab35ddff365ca60f23ed1243690b7eda2)) +* **docs:** grammar improvements ([985f2fe](https://github.com/browserbase/sdk-node/commit/985f2fe1bf866f1dc48c8d5f1827dda2e99b6f11)) +* **docs:** use top-level-await in example snippets ([24c7006](https://github.com/browserbase/sdk-node/commit/24c7006ca4c4dc7c03a48796b2677bdc47728934)) +* improve publish-npm script --latest tag logic ([88ce3e3](https://github.com/browserbase/sdk-node/commit/88ce3e3a96e34bfeb08d2ac254440b283818d7ef)) +* **internal:** make base APIResource abstract ([6ec9d38](https://github.com/browserbase/sdk-node/commit/6ec9d38a92775cb8e46468b05f539f53a01b54c9)) +* **internal:** version bump ([690777f](https://github.com/browserbase/sdk-node/commit/690777f5b88d363db563aba093363ed84c7c9ec4)) + + +### Refactors + +* **types:** replace Record with mapped types ([3e0bfd3](https://github.com/browserbase/sdk-node/commit/3e0bfd327332edd2969106d189e650286d13e441)) + ## 2.6.0 (2025-05-16) Full Changelog: [v2.5.0...v2.6.0](https://github.com/browserbase/sdk-node/compare/v2.5.0...v2.6.0) diff --git a/README.md b/README.md index 5066f7f..4530dc1 100644 --- a/README.md +++ b/README.md @@ -26,13 +26,9 @@ const client = new Browserbase({ apiKey: process.env['BROWSERBASE_API_KEY'], // This is the default and can be omitted }); -async function main() { - const session = await client.sessions.create({ projectId: 'your_project_id' }); +const session = await client.sessions.create({ projectId: 'your_project_id' }); - console.log(session.id); -} - -main(); +console.log(session.id); ``` ### Request & Response types @@ -47,12 +43,8 @@ const client = new Browserbase({ apiKey: process.env['BROWSERBASE_API_KEY'], // This is the default and can be omitted }); -async function main() { - const params: Browserbase.SessionCreateParams = { projectId: 'your_project_id' }; - const session: Browserbase.SessionCreateResponse = await client.sessions.create(params); -} - -main(); +const params: Browserbase.SessionCreateParams = { projectId: 'your_project_id' }; +const session: Browserbase.SessionCreateResponse = await client.sessions.create(params); ``` Documentation for each method, request param, and response field are available in docstrings and will appear on hover in most modern editors. @@ -95,19 +87,15 @@ a subclass of `APIError` will be thrown: ```ts -async function main() { - const session = await client.sessions.create({ projectId: 'your_project_id' }).catch(async (err) => { - if (err instanceof Browserbase.APIError) { - console.log(err.status); // 400 - console.log(err.name); // BadRequestError - console.log(err.headers); // {server: 'nginx', ...} - } else { - throw err; - } - }); -} - -main(); +const session = await client.sessions.create({ projectId: 'your_project_id' }).catch(async (err) => { + if (err instanceof Browserbase.APIError) { + console.log(err.status); // 400 + console.log(err.name); // BadRequestError + console.log(err.headers); // {server: 'nginx', ...} + } else { + throw err; + } +}); ``` Error codes are as follows: diff --git a/SECURITY.md b/SECURITY.md index e10eb19..ad64e4b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -16,11 +16,11 @@ before making any information public. ## Reporting Non-SDK Related Security Issues If you encounter security issues that are not directly related to SDKs but pertain to the services -or products provided by Browserbase please follow the respective company's security reporting guidelines. +or products provided by Browserbase, please follow the respective company's security reporting guidelines. ### Browserbase Terms and Policies -Please contact support@browserbase.com for any questions or concerns regarding security of our services. +Please contact support@browserbase.com for any questions or concerns regarding the security of our services. --- diff --git a/bin/check-release-environment b/bin/check-release-environment index 3098216..e4b6d58 100644 --- a/bin/check-release-environment +++ b/bin/check-release-environment @@ -3,7 +3,7 @@ errors=() if [ -z "${NPM_TOKEN}" ]; then - errors+=("The BROWSERBASE_NPM_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets") + errors+=("The NPM_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets") fi lenErrors=${#errors[@]} diff --git a/bin/publish-npm b/bin/publish-npm index 4c21181..fa2243d 100644 --- a/bin/publish-npm +++ b/bin/publish-npm @@ -4,19 +4,55 @@ set -eux npm config set '//registry.npmjs.org/:_authToken' "$NPM_TOKEN" -# Build the project yarn build - -# Navigate to the dist directory cd dist -# Get the version from package.json -VERSION="$(node -p "require('./package.json').version")" +# Get package name and version from package.json +PACKAGE_NAME="$(jq -r -e '.name' ./package.json)" +VERSION="$(jq -r -e '.version' ./package.json)" + +# Get latest version from npm +# +# If the package doesn't exist, npm will return: +# { +# "error": { +# "code": "E404", +# "summary": "Unpublished on 2025-06-05T09:54:53.528Z", +# "detail": "'the_package' is not in this registry..." +# } +# } +NPM_INFO="$(npm view "$PACKAGE_NAME" version --json 2>/dev/null || true)" + +# Check if we got an E404 error +if echo "$NPM_INFO" | jq -e '.error.code == "E404"' > /dev/null 2>&1; then + # Package doesn't exist yet, no last version + LAST_VERSION="" +elif echo "$NPM_INFO" | jq -e '.error' > /dev/null 2>&1; then + # Report other errors + echo "ERROR: npm returned unexpected data:" + echo "$NPM_INFO" + exit 1 +else + # Success - get the version + LAST_VERSION=$(echo "$NPM_INFO" | jq -r '.') # strip quotes +fi -# Extract the pre-release tag if it exists +# Check if current version is pre-release (e.g. alpha / beta / rc) +CURRENT_IS_PRERELEASE=false if [[ "$VERSION" =~ -([a-zA-Z]+) ]]; then - # Extract the part before any dot in the pre-release identifier - TAG="${BASH_REMATCH[1]}" + CURRENT_IS_PRERELEASE=true + CURRENT_TAG="${BASH_REMATCH[1]}" +fi + +# Check if last version is a stable release +LAST_IS_STABLE_RELEASE=true +if [[ -z "$LAST_VERSION" || "$LAST_VERSION" =~ -([a-zA-Z]+) ]]; then + LAST_IS_STABLE_RELEASE=false +fi + +# Use a corresponding alpha/beta tag if there already is a stable release and we're publishing a prerelease. +if $CURRENT_IS_PRERELEASE && $LAST_IS_STABLE_RELEASE; then + TAG="$CURRENT_TAG" else TAG="latest" fi diff --git a/package-lock.json b/package-lock.json index 4edad49..03380ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@browserbasehq/sdk", - "version": "2.6.0", + "version": "2.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@browserbasehq/sdk", - "version": "2.6.0", + "version": "2.7.0", "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", diff --git a/package.json b/package.json index 39c08ec..191d760 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@browserbasehq/sdk", - "version": "2.6.0", + "version": "2.7.0", "description": "The official Node.js library for the Browserbase API", "author": "Browserbase ", "types": "dist/index.d.ts", diff --git a/scripts/build b/scripts/build index 304bc38..2723a3f 100755 --- a/scripts/build +++ b/scripts/build @@ -28,7 +28,7 @@ fi node scripts/utils/make-dist-package-json.cjs > dist/package.json # build to .js/.mjs/.d.ts files -npm exec tsc-multi +./node_modules/.bin/tsc-multi # copy over handwritten .js/.mjs/.d.ts files cp src/_shims/*.{d.ts,js,mjs,md} dist/_shims cp src/_shims/auto/*.{d.ts,js,mjs} dist/_shims/auto diff --git a/src/core.ts b/src/core.ts index 180db7d..b5f8d89 100644 --- a/src/core.ts +++ b/src/core.ts @@ -170,6 +170,7 @@ export class APIPromise extends Promise { export abstract class APIClient { baseURL: string; + #baseURLOverridden: boolean; maxRetries: number; timeout: number; httpAgent: Agent | undefined; @@ -179,18 +180,21 @@ export abstract class APIClient { constructor({ baseURL, + baseURLOverridden, maxRetries = 2, timeout = 60000, // 1 minute httpAgent, fetch: overriddenFetch, }: { baseURL: string; + baseURLOverridden: boolean; maxRetries?: number | undefined; timeout: number | undefined; httpAgent: Agent | undefined; fetch: Fetch | undefined; }) { this.baseURL = baseURL; + this.#baseURLOverridden = baseURLOverridden; this.maxRetries = validatePositiveInteger('maxRetries', maxRetries); this.timeout = validatePositiveInteger('timeout', timeout); this.httpAgent = httpAgent; @@ -300,7 +304,7 @@ export abstract class APIClient { { retryCount = 0 }: { retryCount?: number } = {}, ): { req: RequestInit; url: string; timeout: number } { const options = { ...inputOptions }; - const { method, path, query, headers: headers = {} } = options; + const { method, path, query, defaultBaseURL, headers: headers = {} } = options; const body = ArrayBuffer.isView(options.body) || (options.__binaryRequest && typeof options.body === 'string') ? @@ -310,7 +314,7 @@ export abstract class APIClient { : null; const contentLength = this.calculateContentLength(body); - const url = this.buildURL(path!, query); + const url = this.buildURL(path!, query, defaultBaseURL); if ('timeout' in options) validatePositiveInteger('timeout', options.timeout); options.timeout = options.timeout ?? this.timeout; const httpAgent = options.httpAgent ?? this.httpAgent ?? getDefaultAgent(url); @@ -503,11 +507,12 @@ export abstract class APIClient { return new PagePromise(this, request, Page); } - buildURL(path: string, query: Req | null | undefined): string { + buildURL(path: string, query: Req | null | undefined, defaultBaseURL?: string | undefined): string { + const baseURL = (!this.#baseURLOverridden && defaultBaseURL) || this.baseURL; const url = isAbsoluteURL(path) ? new URL(path) - : new URL(this.baseURL + (this.baseURL.endsWith('/') && path.startsWith('/') ? path.slice(1) : path)); + : new URL(baseURL + (baseURL.endsWith('/') && path.startsWith('/') ? path.slice(1) : path)); const defaultQuery = this.defaultQuery(); if (!isEmptyObj(defaultQuery)) { @@ -792,6 +797,7 @@ export type RequestOptions< query?: Req | undefined; body?: Req | null | undefined; headers?: Headers | undefined; + defaultBaseURL?: string | undefined; maxRetries?: number; stream?: boolean | undefined; @@ -813,6 +819,7 @@ const requestOptionsKeys: KeysEnum = { query: true, body: true, headers: true, + defaultBaseURL: true, maxRetries: true, stream: true, diff --git a/src/index.ts b/src/index.ts index 594b6a0..694ca07 100644 --- a/src/index.ts +++ b/src/index.ts @@ -128,6 +128,7 @@ export class Browserbase extends Core.APIClient { super({ baseURL: options.baseURL!, + baseURLOverridden: baseURL ? baseURL !== 'https://api.browserbase.com' : false, timeout: options.timeout ?? 60000 /* 1 minute */, httpAgent: options.httpAgent, maxRetries: options.maxRetries, @@ -144,6 +145,13 @@ export class Browserbase extends Core.APIClient { projects: API.Projects = new API.Projects(this); sessions: API.Sessions = new API.Sessions(this); + /** + * Check whether the base URL is set to its default. + */ + #baseURLOverridden(): boolean { + return this.baseURL !== 'https://api.browserbase.com'; + } + protected override defaultQuery(): Core.DefaultQuery | undefined { return this._options.defaultQuery; } diff --git a/src/resource.ts b/src/resource.ts index 36c4573..0fa4172 100644 --- a/src/resource.ts +++ b/src/resource.ts @@ -2,7 +2,7 @@ import type { Browserbase } from './index'; -export class APIResource { +export abstract class APIResource { protected _client: Browserbase; constructor(client: Browserbase) { diff --git a/src/resources/sessions/logs.ts b/src/resources/sessions/logs.ts index 4a67cc6..3907d4f 100644 --- a/src/resources/sessions/logs.ts +++ b/src/resources/sessions/logs.ts @@ -35,7 +35,7 @@ export interface SessionLog { export namespace SessionLog { export interface Request { - params: Record; + params: { [key: string]: unknown }; rawBody: string; @@ -48,7 +48,7 @@ export namespace SessionLog { export interface Response { rawBody: string; - result: Record; + result: { [key: string]: unknown }; /** * milliseconds that have elapsed since the UNIX epoch diff --git a/src/resources/sessions/recording.ts b/src/resources/sessions/recording.ts index feb056f..62f91ca 100644 --- a/src/resources/sessions/recording.ts +++ b/src/resources/sessions/recording.ts @@ -17,7 +17,7 @@ export interface SessionRecording { * See * [rrweb documentation](https://github.com/rrweb-io/rrweb/blob/master/docs/recipes/dive-into-event.md). */ - data: Record; + data: { [key: string]: unknown }; sessionId: string; diff --git a/src/resources/sessions/sessions.ts b/src/resources/sessions/sessions.ts index 8e9c6d0..f604aef 100644 --- a/src/resources/sessions/sessions.ts +++ b/src/resources/sessions/sessions.ts @@ -116,7 +116,7 @@ export interface Session { * Arbitrary user metadata to attach to the session. To learn more about user * metadata, see [User Metadata](/features/sessions#user-metadata). */ - userMetadata?: Record; + userMetadata?: { [key: string]: unknown }; } export interface SessionLiveURLs { @@ -214,7 +214,7 @@ export interface SessionCreateResponse { * Arbitrary user metadata to attach to the session. To learn more about user * metadata, see [User Metadata](/features/sessions#user-metadata). */ - userMetadata?: Record; + userMetadata?: { [key: string]: unknown }; } export interface SessionRetrieveResponse { @@ -286,7 +286,7 @@ export interface SessionRetrieveResponse { * Arbitrary user metadata to attach to the session. To learn more about user * metadata, see [User Metadata](/features/sessions#user-metadata). */ - userMetadata?: Record; + userMetadata?: { [key: string]: unknown }; } export type SessionListResponse = Array; @@ -335,7 +335,7 @@ export interface SessionCreateParams { * Arbitrary user metadata to attach to the session. To learn more about user * metadata, see [User Metadata](/features/sessions#user-metadata). */ - userMetadata?: Record; + userMetadata?: { [key: string]: unknown }; } export namespace SessionCreateParams { diff --git a/src/version.ts b/src/version.ts index cc6e4a8..9556c21 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = '2.6.0'; // x-release-please-version +export const VERSION = '2.7.0'; // x-release-please-version diff --git a/tests/index.test.ts b/tests/index.test.ts index 9d1c3b2..8a0df9f 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -185,6 +185,28 @@ describe('instantiate client', () => { const client = new Browserbase({ apiKey: 'My API Key' }); expect(client.baseURL).toEqual('https://api.browserbase.com'); }); + + test('in request options', () => { + const client = new Browserbase({ apiKey: 'My API Key' }); + expect(client.buildURL('/foo', null, 'http://localhost:5000/option')).toEqual( + 'http://localhost:5000/option/foo', + ); + }); + + test('in request options overridden by client options', () => { + const client = new Browserbase({ apiKey: 'My API Key', baseURL: 'http://localhost:5000/client' }); + expect(client.buildURL('/foo', null, 'http://localhost:5000/option')).toEqual( + 'http://localhost:5000/client/foo', + ); + }); + + test('in request options overridden by env variable', () => { + process.env['BROWSERBASE_BASE_URL'] = 'http://localhost:5000/env'; + const client = new Browserbase({ apiKey: 'My API Key' }); + expect(client.buildURL('/foo', null, 'http://localhost:5000/option')).toEqual( + 'http://localhost:5000/env/foo', + ); + }); }); test('maxRetries option is correctly set', () => {