diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml
index 138923f..5bd7c7c 100644
--- a/.github/workflows/release-please.yml
+++ b/.github/workflows/release-please.yml
@@ -1,12 +1,21 @@
name: Release Please
+
+# `next` accumulates validated SDK changes in one versioned PR to `main`.
+# Merging that PR creates the GitHub release; the package publishing workflow
+# runs from the release event.
on:
push:
branches:
+ - next
- main
permissions:
contents: read
+concurrency:
+ group: release-please
+ cancel-in-progress: false
+
jobs:
release-please:
if: github.repository == 'kernel/kernel-go-sdk'
@@ -26,7 +35,68 @@ jobs:
permission-pull-requests: write
permission-workflows: write
- - uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 # v4.4.1
- id: release
+ - name: Set up Node
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ with:
+ node-version: '18.20.2'
+
+ - name: Set up pnpm
+ uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4
with:
- token: ${{ steps.release-token.outputs.token }}
+ version: '9.11.0'
+ run_install: false
+
+ - name: Build pinned release tooling
+ id: tooling
+ env:
+ RELEASE_PLEASE_DIR: ${{ runner.temp }}/release-please
+ RELEASE_PLEASE_SHA: a116e1e520e0f87824acf46a2e79c91d41e819d7
+ run: |
+ set -euo pipefail
+ rm -rf "$RELEASE_PLEASE_DIR"
+ git init "$RELEASE_PLEASE_DIR"
+ git -C "$RELEASE_PLEASE_DIR" remote add origin https://github.com/stainless-api/release-please.git
+ git -C "$RELEASE_PLEASE_DIR" fetch --depth=1 origin "$RELEASE_PLEASE_SHA"
+ git -C "$RELEASE_PLEASE_DIR" checkout --detach FETCH_HEAD
+ pnpm --dir "$RELEASE_PLEASE_DIR" install --frozen-lockfile
+ pnpm --dir "$RELEASE_PLEASE_DIR" build
+ echo "cli=$RELEASE_PLEASE_DIR/build/src/bin/release-please.js" >> "$GITHUB_OUTPUT"
+
+ - name: Open or update the release PR
+ if: github.ref_name == 'next'
+ env:
+ GH_TOKEN: ${{ steps.release-token.outputs.token }}
+ RELEASE_PLEASE: ${{ steps.tooling.outputs.cli }}
+ run: |
+ set -euo pipefail
+ node "$RELEASE_PLEASE" release-pr \
+ --repo-url "$GITHUB_REPOSITORY" \
+ --token "$GH_TOKEN" \
+ --target-branch main \
+ --changes-branch next
+
+ - name: Remove the legacy promotion PR
+ if: github.ref_name == 'next'
+ env:
+ GH_TOKEN: ${{ steps.release-token.outputs.token }}
+ run: |
+ set -euo pipefail
+ legacy=$(gh pr list --repo "$GITHUB_REPOSITORY" --head stainless/release \
+ --state open --json number --jq '.[].number')
+ for pr in $legacy; do
+ gh pr close "$pr" --repo "$GITHUB_REPOSITORY" \
+ --comment "Superseded by the versioned release PR from next to main."
+ done
+ gh api -X DELETE "repos/$GITHUB_REPOSITORY/git/refs/heads/stainless/release" >/dev/null 2>&1 || true
+
+ - name: Create the GitHub release
+ if: github.ref_name == 'main'
+ env:
+ GH_TOKEN: ${{ steps.release-token.outputs.token }}
+ RELEASE_PLEASE: ${{ steps.tooling.outputs.cli }}
+ run: |
+ set -euo pipefail
+ node "$RELEASE_PLEASE" github-release \
+ --repo-url "$GITHUB_REPOSITORY" \
+ --token "$GH_TOKEN" \
+ --target-branch main
diff --git a/.github/workflows/stlc-promote.yml b/.github/workflows/stlc-promote.yml
index 5d446ce..7f0650f 100644
--- a/.github/workflows/stlc-promote.yml
+++ b/.github/workflows/stlc-promote.yml
@@ -1,8 +1,9 @@
-name: Promote SDKs
+name: Promote SDK changes
-# Production requires pull requests, so staging is promoted through a merge-
-# commit PR. Never squash or rebase this cross-repo PR: preserving the incoming
-# commits keeps production and staging on one ancestry chain.
+# Staging is the generator's integration history. Production `next` is the
+# developer-facing queue for the next release. This workflow combines the
+# latest released state with validated staging changes, then advances `next`.
+# Release automation maintains the single versioned PR from `next` to `main`.
on:
push:
branches: [main]
@@ -34,50 +35,99 @@ jobs:
owner: kernel
repositories: kernel-go-sdk
permission-contents: write
- permission-workflows: write
permission-pull-requests: write
+ permission-workflows: write
- - name: Fetch production main
+ - name: Fetch production branches
+ id: production
env:
GH_TOKEN: ${{ steps.production-token.outputs.token }}
PRODUCTION_REPO: kernel/kernel-go-sdk
run: |
- git remote add production "https://x-access-token:${GH_TOKEN}@github.com/${PRODUCTION_REPO}.git"
+ set -euo pipefail
+ git remote add production \
+ "https://x-access-token:${GH_TOKEN}@github.com/${PRODUCTION_REPO}.git"
git fetch production main
-
- - name: Check whether production already has staging's content
- id: diff
- run: |
- MERGED=$(git merge-tree --write-tree production/main origin/main) || MERGED=conflict
- PRODUCTION_TREE=$(git rev-parse 'production/main^{tree}')
- if [ "$MERGED" = "$PRODUCTION_TREE" ]; then
- echo "Production already contains staging's content. Nothing to promote."
- echo "synced=true" >> "$GITHUB_OUTPUT"
+ if git ls-remote --exit-code --heads production next >/dev/null 2>&1; then
+ git fetch production next
+ echo "has_next=true" >> "$GITHUB_OUTPUT"
else
- echo "synced=false" >> "$GITHUB_OUTPUT"
+ echo "has_next=false" >> "$GITHUB_OUTPUT"
fi
- - name: Push the production release branch
- if: steps.diff.outputs.synced == 'false'
- env:
- GH_TOKEN: ${{ steps.production-token.outputs.token }}
- PRODUCTION_REPO: kernel/kernel-go-sdk
- run: git push production origin/main:refs/heads/stainless/release --force
-
- - name: Open or update the promote PR
- if: steps.diff.outputs.synced == 'false'
+ - name: Prepare the next release branch
env:
+ APP_SLUG: ${{ steps.production-token.outputs.app-slug }}
GH_TOKEN: ${{ steps.production-token.outputs.token }}
+ HAS_NEXT: ${{ steps.production.outputs.has_next }}
PRODUCTION_REPO: kernel/kernel-go-sdk
run: |
- body=$(mktemp)
- git log --oneline production/main..origin/main > "$body"
- existing=$(gh pr list --repo "$PRODUCTION_REPO" --head stainless/release --state open --json number --jq 'if length == 0 then "" else .[0].number end')
- if [ -z "$existing" ]; then
- gh pr create --repo "$PRODUCTION_REPO" --base main --head stainless/release --title "Release SDK updates" --body-file "$body"
+ set -euo pipefail
+ bot_id=$(gh api "/users/${APP_SLUG}[bot]" --jq .id)
+ git config user.name "${APP_SLUG}[bot]"
+ git config user.email "${bot_id}+${APP_SLUG}[bot]@users.noreply.github.com"
+
+ open_conflict_pr() {
+ source_ref=$1
+ source_name=$2
+ advance_next=$3
+ conflict_branch=stlc/promotion-conflict
+
+ git merge --abort
+ existing=$(gh pr list --repo "$PRODUCTION_REPO" --base next \
+ --head "$conflict_branch" --state open --json url --jq '.[0].url // ""')
+ if [ -n "$existing" ]; then
+ echo "::error title=SDK promotion blocked::Resolve the existing recovery PR: $existing"
+ exit 1
+ fi
+
+ if [ "$advance_next" = "true" ]; then
+ git push production HEAD:refs/heads/next
+ fi
+ git push production "$source_ref:refs/heads/$conflict_branch" --force
+
+ body=$(mktemp)
+ printf '%s\n' \
+ '## SDK promotion conflict' \
+ '' \
+ "The automated promotion could not merge $source_name into the pending next release." \
+ '' \
+ 'Resolve the conflicts on this branch, validate the SDK, mark this PR ready, and merge it with a merge commit.' \
+ '' \
+ 'After merging, rerun the staging Promote SDK changes workflow to include any newer generated changes.' \
+ > "$body"
+ recovery_url=$(gh pr create --repo "$PRODUCTION_REPO" --draft \
+ --base next --head "$conflict_branch" \
+ --title 'chore: resolve SDK promotion conflict' --body-file "$body")
+ echo "::error title=SDK promotion conflict::Resolve the recovery PR: $recovery_url"
+ exit 1
+ }
+
+ if [ "$HAS_NEXT" = "true" ]; then
+ git checkout -B stlc/promote-next production/next
else
- gh pr edit "$existing" --repo "$PRODUCTION_REPO" --title "Release SDK updates" --body-file "$body"
+ git checkout -B stlc/promote-next production/main
+ fi
+
+ if ! git merge-base --is-ancestor production/main HEAD; then
+ if ! git merge --no-edit production/main; then
+ open_conflict_pr production/main 'production main' false
+ fi
+ fi
+ if ! git merge-base --is-ancestor origin/main HEAD; then
+ if ! git merge --no-edit origin/main; then
+ open_conflict_pr origin/main 'validated staging changes' true
+ fi
fi
- if ! gh pr merge stainless/release --repo "$PRODUCTION_REPO" --merge --auto; then
- echo "::warning title=Manual promotion required::Merge the promote PR with a merge commit."
+
+ if [ "$HAS_NEXT" = "true" ]; then
+ git merge-base --is-ancestor production/next HEAD
fi
+
+ - name: Update the pending release
+ env:
+ GH_TOKEN: ${{ steps.production-token.outputs.token }}
+ run: |
+ set -euo pipefail
+ git push production HEAD:refs/heads/next
+ echo "Updated production next; the versioned release PR will be opened or refreshed."
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index d80a91e..6af24e3 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.88.0"
+ ".": "0.89.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2bcd95a..30e4789 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
# Changelog
+## [0.89.0](https://github.com/kernel/kernel-go-sdk/compare/v0.88.0...v0.89.0) (2026-08-12)
+
+
+### Features
+
+* Add region as a first-class API field with plan and flag gating ([fd01c36](https://github.com/kernel/kernel-go-sdk/commit/fd01c36a04c73192f29d4803e46bc2b6e004a9b9))
+* Add typed network config with private_hosts to browsers and pools ([9f0076b](https://github.com/kernel/kernel-go-sdk/commit/9f0076b20dc2f81709091497a89d587e996f75ee))
+* Expose plan-derived auth limits on GET /org/limits ([25ccf2e](https://github.com/kernel/kernel-go-sdk/commit/25ccf2ec8b8b38f63573f6d7817b725585aa0d3e))
+
## [0.88.0](https://github.com/kernel/kernel-go-sdk/compare/v0.87.0...v0.88.0) (2026-08-10)
### Features
diff --git a/README.md b/README.md
index 11c74af..de6b76e 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,7 @@ Or to pin the version:
```sh
-go get -u 'github.com/kernel/kernel-go-sdk@v0.88.0'
+go get -u 'github.com/kernel/kernel-go-sdk@v0.89.0'
```
diff --git a/api.md b/api.md
index 7bb412f..b33173f 100644
--- a/api.md
+++ b/api.md
@@ -69,12 +69,14 @@ Methods:
Params Types:
+- kernel.BrowserNetworkConfigParam
- kernel.BrowserProxyConfigParam
- kernel.BrowserProxyMode
- kernel.Tags
Response Types:
+- kernel.BrowserNetworkConfig
- kernel.BrowserPoolRef
- kernel.BrowserProxy
- kernel.BrowserProxyConfig
diff --git a/browser.go b/browser.go
index bcdca34..c65dc51 100644
--- a/browser.go
+++ b/browser.go
@@ -166,6 +166,90 @@ func (r *BrowserService) LoadExtensions(ctx context.Context, id string, body Bro
return err
}
+// Network configuration for a browser session or browser pool.
+type BrowserNetworkConfig struct {
+ // Destinations the browser reaches directly through the session's own network
+ // instead of through Kernel-managed egress — for private hosts reachable over a
+ // VPN or tunnel the session has joined (e.g. a Tailscale tailnet). By default,
+ // private IP ranges already route directly: RFC1918 (10.0.0.0/8, 172.16.0.0/12,
+ // 192.168.0.0/16), CGNAT/Tailscale (100.64.0.0/10), and IPv6 ULA (fc00::/7). An
+ // explicitly supplied list replaces those defaults with exactly the entries given,
+ // and an empty list ([]) disables them so all traffic uses Kernel-managed egress;
+ // omit private_hosts to keep the defaults. Entries are hostname patterns
+ // ("_.example.ts.net", "preview.internal") or IP/CIDR literals ("100.64.0.0/10",
+ // "10.1.30.63"). IP and CIDR entries only match URLs written with a literal IP
+ // address; they never match hostnames that resolve into the range, so private DNS
+ // names need a hostname entry even when they resolve inside the default ranges.
+ // CIDRs must be in canonical masked form (host bits zero), and only the private
+ // ranges listed above are accepted; public, loopback, link-local, and unspecified
+ // ranges are rejected. Exact IPv6 addresses must be bracketed ("[fd00::1]"); IPv6
+ // CIDR ranges are unbracketed ("fd00::/8"). Wildcards are limited to one leading
+ // "_." over a suffix with at least two labels that is not a public suffix (so
+ // "_.co.uk" or "_.ts.net" are rejected, while "\*.example.ts.net" is accepted).
+ // Hostname and IP entries may carry a port; CIDR ranges may not. Hostname entries
+ // are not resolved during validation, so callers must ensure they identify private
+ // destinations. Not related to a proxy's bypass_hosts, which selects between
+ // upstream-proxy and Kernel-managed direct egress and cannot reach into a VPN.
+ PrivateHosts []string `json:"private_hosts"`
+ // JSON contains metadata for fields, check presence with [respjson.Field.Valid].
+ JSON struct {
+ PrivateHosts respjson.Field
+ ExtraFields map[string]respjson.Field
+ raw string
+ } `json:"-"`
+}
+
+// Returns the unmodified JSON received from the API
+func (r BrowserNetworkConfig) RawJSON() string { return r.JSON.raw }
+func (r *BrowserNetworkConfig) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+// ToParam converts this BrowserNetworkConfig to a BrowserNetworkConfigParam.
+//
+// Warning: the fields of the param type will not be present. ToParam should only
+// be used at the last possible moment before sending a request. Test for this with
+// BrowserNetworkConfigParam.Overrides()
+func (r BrowserNetworkConfig) ToParam() BrowserNetworkConfigParam {
+ return param.Override[BrowserNetworkConfigParam](json.RawMessage(r.RawJSON()))
+}
+
+// Network configuration for a browser session or browser pool.
+type BrowserNetworkConfigParam struct {
+ // Destinations the browser reaches directly through the session's own network
+ // instead of through Kernel-managed egress — for private hosts reachable over a
+ // VPN or tunnel the session has joined (e.g. a Tailscale tailnet). By default,
+ // private IP ranges already route directly: RFC1918 (10.0.0.0/8, 172.16.0.0/12,
+ // 192.168.0.0/16), CGNAT/Tailscale (100.64.0.0/10), and IPv6 ULA (fc00::/7). An
+ // explicitly supplied list replaces those defaults with exactly the entries given,
+ // and an empty list ([]) disables them so all traffic uses Kernel-managed egress;
+ // omit private_hosts to keep the defaults. Entries are hostname patterns
+ // ("_.example.ts.net", "preview.internal") or IP/CIDR literals ("100.64.0.0/10",
+ // "10.1.30.63"). IP and CIDR entries only match URLs written with a literal IP
+ // address; they never match hostnames that resolve into the range, so private DNS
+ // names need a hostname entry even when they resolve inside the default ranges.
+ // CIDRs must be in canonical masked form (host bits zero), and only the private
+ // ranges listed above are accepted; public, loopback, link-local, and unspecified
+ // ranges are rejected. Exact IPv6 addresses must be bracketed ("[fd00::1]"); IPv6
+ // CIDR ranges are unbracketed ("fd00::/8"). Wildcards are limited to one leading
+ // "_." over a suffix with at least two labels that is not a public suffix (so
+ // "_.co.uk" or "_.ts.net" are rejected, while "\*.example.ts.net" is accepted).
+ // Hostname and IP entries may carry a port; CIDR ranges may not. Hostname entries
+ // are not resolved during validation, so callers must ensure they identify private
+ // destinations. Not related to a proxy's bypass_hosts, which selects between
+ // upstream-proxy and Kernel-managed direct egress and cannot reach into a VPN.
+ PrivateHosts []string `json:"private_hosts,omitzero"`
+ paramObj
+}
+
+func (r BrowserNetworkConfigParam) MarshalJSON() (data []byte, err error) {
+ type shadow BrowserNetworkConfigParam
+ return param.MarshalObject(r, (*shadow)(&r))
+}
+func (r *BrowserNetworkConfigParam) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
// Browser pool this session was acquired from, if any.
type BrowserPoolRef struct {
// Browser pool ID
@@ -369,6 +453,10 @@ type BrowserNewResponse struct {
CreatedAt time.Time `json:"created_at" api:"required" format:"date-time"`
// Whether the browser session is running in headless mode.
Headless bool `json:"headless" api:"required"`
+ // Geographic region of the browser session. Fixed once the session is created.
+ //
+ // Any of "us-east", "eu-west".
+ Region BrowserNewResponseRegion `json:"region" api:"required"`
// Unique identifier for the browser session
SessionID string `json:"session_id" api:"required"`
// Whether the browser session is running in stealth mode.
@@ -395,6 +483,9 @@ type BrowserNewResponse struct {
KioskMode bool `json:"kiosk_mode"`
// Human-readable name of the browser session, if one was set at creation.
Name string `json:"name"`
+ // Network configuration the session was created with, if any. Omitted when the
+ // session has no network configuration.
+ Network BrowserNetworkConfig `json:"network"`
// Browser pool this session was acquired from, if any.
Pool BrowserPoolRef `json:"pool"`
// Browser profile metadata.
@@ -440,6 +531,7 @@ type BrowserNewResponse struct {
CdpWsURL respjson.Field
CreatedAt respjson.Field
Headless respjson.Field
+ Region respjson.Field
SessionID respjson.Field
Stealth respjson.Field
TimeoutSeconds respjson.Field
@@ -451,6 +543,7 @@ type BrowserNewResponse struct {
GPU respjson.Field
KioskMode respjson.Field
Name respjson.Field
+ Network respjson.Field
Pool respjson.Field
Profile respjson.Field
ProfileSaveChanges respjson.Field
@@ -472,6 +565,14 @@ func (r *BrowserNewResponse) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}
+// Geographic region of the browser session. Fixed once the session is created.
+type BrowserNewResponseRegion string
+
+const (
+ BrowserNewResponseRegionUsEast BrowserNewResponseRegion = "us-east"
+ BrowserNewResponseRegionEuWest BrowserNewResponseRegion = "eu-west"
+)
+
type BrowserGetResponse struct {
// Websocket URL for Chrome DevTools Protocol connections to the browser session
CdpWsURL string `json:"cdp_ws_url" api:"required"`
@@ -479,6 +580,10 @@ type BrowserGetResponse struct {
CreatedAt time.Time `json:"created_at" api:"required" format:"date-time"`
// Whether the browser session is running in headless mode.
Headless bool `json:"headless" api:"required"`
+ // Geographic region of the browser session. Fixed once the session is created.
+ //
+ // Any of "us-east", "eu-west".
+ Region BrowserGetResponseRegion `json:"region" api:"required"`
// Unique identifier for the browser session
SessionID string `json:"session_id" api:"required"`
// Whether the browser session is running in stealth mode.
@@ -505,6 +610,9 @@ type BrowserGetResponse struct {
KioskMode bool `json:"kiosk_mode"`
// Human-readable name of the browser session, if one was set at creation.
Name string `json:"name"`
+ // Network configuration the session was created with, if any. Omitted when the
+ // session has no network configuration.
+ Network BrowserNetworkConfig `json:"network"`
// Browser pool this session was acquired from, if any.
Pool BrowserPoolRef `json:"pool"`
// Browser profile metadata.
@@ -550,6 +658,7 @@ type BrowserGetResponse struct {
CdpWsURL respjson.Field
CreatedAt respjson.Field
Headless respjson.Field
+ Region respjson.Field
SessionID respjson.Field
Stealth respjson.Field
TimeoutSeconds respjson.Field
@@ -561,6 +670,7 @@ type BrowserGetResponse struct {
GPU respjson.Field
KioskMode respjson.Field
Name respjson.Field
+ Network respjson.Field
Pool respjson.Field
Profile respjson.Field
ProfileSaveChanges respjson.Field
@@ -582,6 +692,14 @@ func (r *BrowserGetResponse) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}
+// Geographic region of the browser session. Fixed once the session is created.
+type BrowserGetResponseRegion string
+
+const (
+ BrowserGetResponseRegionUsEast BrowserGetResponseRegion = "us-east"
+ BrowserGetResponseRegionEuWest BrowserGetResponseRegion = "eu-west"
+)
+
type BrowserUpdateResponse struct {
// Websocket URL for Chrome DevTools Protocol connections to the browser session
CdpWsURL string `json:"cdp_ws_url" api:"required"`
@@ -589,6 +707,10 @@ type BrowserUpdateResponse struct {
CreatedAt time.Time `json:"created_at" api:"required" format:"date-time"`
// Whether the browser session is running in headless mode.
Headless bool `json:"headless" api:"required"`
+ // Geographic region of the browser session. Fixed once the session is created.
+ //
+ // Any of "us-east", "eu-west".
+ Region BrowserUpdateResponseRegion `json:"region" api:"required"`
// Unique identifier for the browser session
SessionID string `json:"session_id" api:"required"`
// Whether the browser session is running in stealth mode.
@@ -615,6 +737,9 @@ type BrowserUpdateResponse struct {
KioskMode bool `json:"kiosk_mode"`
// Human-readable name of the browser session, if one was set at creation.
Name string `json:"name"`
+ // Network configuration the session was created with, if any. Omitted when the
+ // session has no network configuration.
+ Network BrowserNetworkConfig `json:"network"`
// Browser pool this session was acquired from, if any.
Pool BrowserPoolRef `json:"pool"`
// Browser profile metadata.
@@ -660,6 +785,7 @@ type BrowserUpdateResponse struct {
CdpWsURL respjson.Field
CreatedAt respjson.Field
Headless respjson.Field
+ Region respjson.Field
SessionID respjson.Field
Stealth respjson.Field
TimeoutSeconds respjson.Field
@@ -671,6 +797,7 @@ type BrowserUpdateResponse struct {
GPU respjson.Field
KioskMode respjson.Field
Name respjson.Field
+ Network respjson.Field
Pool respjson.Field
Profile respjson.Field
ProfileSaveChanges respjson.Field
@@ -692,6 +819,14 @@ func (r *BrowserUpdateResponse) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}
+// Geographic region of the browser session. Fixed once the session is created.
+type BrowserUpdateResponseRegion string
+
+const (
+ BrowserUpdateResponseRegionUsEast BrowserUpdateResponseRegion = "us-east"
+ BrowserUpdateResponseRegionEuWest BrowserUpdateResponseRegion = "eu-west"
+)
+
type BrowserListResponse struct {
// Websocket URL for Chrome DevTools Protocol connections to the browser session
CdpWsURL string `json:"cdp_ws_url" api:"required"`
@@ -699,6 +834,10 @@ type BrowserListResponse struct {
CreatedAt time.Time `json:"created_at" api:"required" format:"date-time"`
// Whether the browser session is running in headless mode.
Headless bool `json:"headless" api:"required"`
+ // Geographic region of the browser session. Fixed once the session is created.
+ //
+ // Any of "us-east", "eu-west".
+ Region BrowserListResponseRegion `json:"region" api:"required"`
// Unique identifier for the browser session
SessionID string `json:"session_id" api:"required"`
// Whether the browser session is running in stealth mode.
@@ -725,6 +864,9 @@ type BrowserListResponse struct {
KioskMode bool `json:"kiosk_mode"`
// Human-readable name of the browser session, if one was set at creation.
Name string `json:"name"`
+ // Network configuration the session was created with, if any. Omitted when the
+ // session has no network configuration.
+ Network BrowserNetworkConfig `json:"network"`
// Browser pool this session was acquired from, if any.
Pool BrowserPoolRef `json:"pool"`
// Browser profile metadata.
@@ -770,6 +912,7 @@ type BrowserListResponse struct {
CdpWsURL respjson.Field
CreatedAt respjson.Field
Headless respjson.Field
+ Region respjson.Field
SessionID respjson.Field
Stealth respjson.Field
TimeoutSeconds respjson.Field
@@ -781,6 +924,7 @@ type BrowserListResponse struct {
GPU respjson.Field
KioskMode respjson.Field
Name respjson.Field
+ Network respjson.Field
Pool respjson.Field
Profile respjson.Field
ProfileSaveChanges respjson.Field
@@ -802,6 +946,14 @@ func (r *BrowserListResponse) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}
+// Geographic region of the browser session. Fixed once the session is created.
+type BrowserListResponseRegion string
+
+const (
+ BrowserListResponseRegionUsEast BrowserListResponseRegion = "us-east"
+ BrowserListResponseRegionEuWest BrowserListResponseRegion = "eu-west"
+)
+
// Structured response from the browser curl request.
type BrowserCurlResponse struct {
// Response body (UTF-8 string or base64 depending on request).
@@ -877,6 +1029,8 @@ type BrowserNewParams struct {
ChromePolicy map[string]any `json:"chrome_policy,omitzero"`
// List of browser extensions to load into the session. Provide each by id or name.
Extensions []shared.BrowserExtensionParam `json:"extensions,omitzero"`
+ // Network configuration for the browser session. Cannot be changed after creation.
+ Network BrowserNetworkConfigParam `json:"network,omitzero"`
// Profile selection for the browser session. Provide either id or name. If
// specified, the matching profile will be loaded into the browser session.
// Profiles must be created beforehand.
@@ -889,6 +1043,12 @@ type BrowserNewParams struct {
// egress when stealth=false. Select id or name to use that proxy regardless of
// stealth. Proxy selection does not change stealth or CAPTCHA solver behavior.
Proxy BrowserProxyConfigParam `json:"proxy,omitzero"`
+ // Geographic region for the browser session. It is fixed once the session is
+ // created. Region selection requires a Start-Up or Enterprise plan, defaults to
+ // us-east when omitted on create.
+ //
+ // Any of "us-east", "eu-west".
+ Region BrowserNewParamsRegion `json:"region,omitzero"`
// Optional user-defined key-value tags for the browser session, used to find and
// group sessions later. Can be changed later via PATCH /browsers/{id_or_name}. Up
// to 50 pairs.
@@ -917,6 +1077,16 @@ func (r *BrowserNewParams) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}
+// Geographic region for the browser session. It is fixed once the session is
+// created. Region selection requires a Start-Up or Enterprise plan, defaults to
+// us-east when omitted on create.
+type BrowserNewParamsRegion string
+
+const (
+ BrowserNewParamsRegionUsEast BrowserNewParamsRegion = "us-east"
+ BrowserNewParamsRegionEuWest BrowserNewParamsRegion = "eu-west"
+)
+
// Telemetry configuration for the browser session. Set enabled to true to start
// capture using VM defaults, or provide browser category settings. If omitted,
// null, set to an empty object ({}), set to enabled: false without browser
@@ -1189,6 +1359,10 @@ type BrowserListParams struct {
Offset param.Opt[int64] `query:"offset,omitzero" json:"-"`
// Search browsers by name, session ID, profile name or ID, proxy ID, or pool name.
Query param.Opt[string] `query:"query,omitzero" json:"-"`
+ // Filter sessions by geographic region. Omit to list sessions in all regions.
+ //
+ // Any of "us-east", "eu-west".
+ Region BrowserListParamsRegion `query:"region,omitzero" json:"-"`
// Filter sessions by status. "active" returns only active sessions (default),
// "deleted" returns only soft-deleted sessions, "all" returns both.
//
@@ -1209,6 +1383,14 @@ func (r BrowserListParams) URLQuery() (v url.Values, err error) {
})
}
+// Filter sessions by geographic region. Omit to list sessions in all regions.
+type BrowserListParamsRegion string
+
+const (
+ BrowserListParamsRegionUsEast BrowserListParamsRegion = "us-east"
+ BrowserListParamsRegionEuWest BrowserListParamsRegion = "eu-west"
+)
+
// Filter sessions by status. "active" returns only active sessions (default),
// "deleted" returns only soft-deleted sessions, "all" returns both.
type BrowserListParamsStatus string
diff --git a/browser_test.go b/browser_test.go
index 57df42a..552f46a 100644
--- a/browser_test.go
+++ b/browser_test.go
@@ -42,6 +42,9 @@ func TestBrowserNewWithOptionalParams(t *testing.T) {
InvocationID: kernel.String("rr33xuugxj9h0bkf1rdt2bet"),
KioskMode: kernel.Bool(true),
Name: kernel.String("checkout-flow-1"),
+ Network: kernel.BrowserNetworkConfigParam{
+ PrivateHosts: []string{"*.example.ts.net", "100.64.0.0/10"},
+ },
Profile: shared.BrowserProfileParam{
ID: kernel.String("id"),
Name: kernel.String("name"),
@@ -53,6 +56,7 @@ func TestBrowserNewWithOptionalParams(t *testing.T) {
Name: kernel.String("x"),
},
ProxyID: kernel.String("proxy_id"),
+ Region: kernel.BrowserNewParamsRegionUsEast,
StartURL: kernel.String("https://example.com"),
Stealth: kernel.Bool(true),
Tags: kernel.Tags{
@@ -257,6 +261,7 @@ func TestBrowserListWithOptionalParams(t *testing.T) {
Limit: kernel.Int(1),
Offset: kernel.Int(0),
Query: kernel.String("query"),
+ Region: kernel.BrowserListParamsRegionUsEast,
Status: kernel.BrowserListParamsStatusActive,
Tags: map[string]string{
"foo": "string",
diff --git a/browserpool.go b/browserpool.go
index cc3a240..db1b640 100644
--- a/browserpool.go
+++ b/browserpool.go
@@ -176,6 +176,10 @@ type BrowserPool struct {
// extensions inside `browser_pool_config` reflect the configured selector (echoed
// as sent on create).
ExtensionIDs []string `json:"extension_ids" api:"required"`
+ // Geographic region of the browser pool. Fixed once the pool is created.
+ //
+ // Any of "us-east", "eu-west".
+ Region BrowserPoolRegion `json:"region" api:"required"`
// Browser pool name, if set
Name string `json:"name"`
// Resolved profile ID the pool is attached to. Omitted when no profile is
@@ -191,6 +195,7 @@ type BrowserPool struct {
BrowserPoolConfig respjson.Field
CreatedAt respjson.Field
ExtensionIDs respjson.Field
+ Region respjson.Field
Name respjson.Field
ProfileID respjson.Field
ExtraFields map[string]respjson.Field
@@ -228,6 +233,9 @@ type BrowserPoolBrowserPoolConfig struct {
KioskMode bool `json:"kiosk_mode"`
// Optional name for the browser pool. Must be unique within the project.
Name string `json:"name"`
+ // Network configuration applied to browsers in this pool, if any. Omitted when the
+ // pool has no network configuration.
+ Network BrowserNetworkConfig `json:"network"`
// Profile configuration for browsers in a pool. Provide either id or name.
// Profiles must be created beforehand. Unlike single browser sessions, pools load
// the profile read-only and never persist changes back to it, so save_changes is
@@ -278,6 +286,7 @@ type BrowserPoolBrowserPoolConfig struct {
Headless respjson.Field
KioskMode respjson.Field
Name respjson.Field
+ Network respjson.Field
Profile respjson.Field
ProxyID respjson.Field
RefreshOnProfileUpdate respjson.Field
@@ -323,6 +332,14 @@ func (r *BrowserPoolBrowserPoolConfigProfile) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}
+// Geographic region of the browser pool. Fixed once the pool is created.
+type BrowserPoolRegion string
+
+const (
+ BrowserPoolRegionUsEast BrowserPoolRegion = "us-east"
+ BrowserPoolRegionEuWest BrowserPoolRegion = "eu-west"
+)
+
type BrowserPoolAcquireResponse struct {
// Websocket URL for Chrome DevTools Protocol connections to the browser session
CdpWsURL string `json:"cdp_ws_url" api:"required"`
@@ -330,6 +347,10 @@ type BrowserPoolAcquireResponse struct {
CreatedAt time.Time `json:"created_at" api:"required" format:"date-time"`
// Whether the browser session is running in headless mode.
Headless bool `json:"headless" api:"required"`
+ // Geographic region of the browser session. Fixed once the session is created.
+ //
+ // Any of "us-east", "eu-west".
+ Region BrowserPoolAcquireResponseRegion `json:"region" api:"required"`
// Unique identifier for the browser session
SessionID string `json:"session_id" api:"required"`
// Whether the browser session is running in stealth mode.
@@ -356,6 +377,9 @@ type BrowserPoolAcquireResponse struct {
KioskMode bool `json:"kiosk_mode"`
// Human-readable name of the browser session, if one was set at creation.
Name string `json:"name"`
+ // Network configuration the session was created with, if any. Omitted when the
+ // session has no network configuration.
+ Network BrowserNetworkConfig `json:"network"`
// Browser pool this session was acquired from, if any.
Pool BrowserPoolRef `json:"pool"`
// Browser profile metadata.
@@ -401,6 +425,7 @@ type BrowserPoolAcquireResponse struct {
CdpWsURL respjson.Field
CreatedAt respjson.Field
Headless respjson.Field
+ Region respjson.Field
SessionID respjson.Field
Stealth respjson.Field
TimeoutSeconds respjson.Field
@@ -412,6 +437,7 @@ type BrowserPoolAcquireResponse struct {
GPU respjson.Field
KioskMode respjson.Field
Name respjson.Field
+ Network respjson.Field
Pool respjson.Field
Profile respjson.Field
ProfileSaveChanges respjson.Field
@@ -433,6 +459,14 @@ func (r *BrowserPoolAcquireResponse) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}
+// Geographic region of the browser session. Fixed once the session is created.
+type BrowserPoolAcquireResponseRegion string
+
+const (
+ BrowserPoolAcquireResponseRegionUsEast BrowserPoolAcquireResponseRegion = "us-east"
+ BrowserPoolAcquireResponseRegionEuWest BrowserPoolAcquireResponseRegion = "eu-west"
+)
+
type BrowserPoolNewParams struct {
// Number of browsers to maintain in the pool. The maximum size is determined by
// your organization's pooled sessions limit (the sum of all pool sizes cannot
@@ -483,12 +517,20 @@ type BrowserPoolNewParams struct {
ChromePolicy map[string]any `json:"chrome_policy,omitzero"`
// List of browser extensions to load into the session. Provide each by id or name.
Extensions []shared.BrowserExtensionParam `json:"extensions,omitzero"`
+ // Network configuration applied to browsers in this pool.
+ Network BrowserNetworkConfigParam `json:"network,omitzero"`
// Profile configuration for browsers in a pool. Provide either id or name.
// Profiles must be created beforehand. Unlike single browser sessions, pools load
// the profile read-only and never persist changes back to it, so save_changes is
// omitted here. Any save_changes value sent on a pool profile is silently ignored
// rather than rejected.
Profile BrowserPoolNewParamsProfile `json:"profile,omitzero"`
+ // Geographic region for the browser pool. It is fixed once the pool is created.
+ // Region selection requires a Start-Up or Enterprise plan, defaults to us-east
+ // when omitted on create.
+ //
+ // Any of "us-east", "eu-west".
+ Region BrowserPoolNewParamsRegion `json:"region,omitzero"`
// Initial browser window size in pixels with optional refresh rate. If omitted,
// image defaults apply (1920x1080@25). For GPU images, the default is
// 1920x1080@60. Arbitrary viewport dimensions and refresh rates are accepted.
@@ -535,6 +577,16 @@ func (r *BrowserPoolNewParamsProfile) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}
+// Geographic region for the browser pool. It is fixed once the pool is created.
+// Region selection requires a Start-Up or Enterprise plan, defaults to us-east
+// when omitted on create.
+type BrowserPoolNewParamsRegion string
+
+const (
+ BrowserPoolNewParamsRegionUsEast BrowserPoolNewParamsRegion = "us-east"
+ BrowserPoolNewParamsRegionEuWest BrowserPoolNewParamsRegion = "eu-west"
+)
+
// Telemetry configuration applied to browsers warmed into this pool. Set enabled
// to true to start capture using the default set, or provide browser category
// settings. If omitted, null, set to an empty object ({}), set to enabled: false
@@ -688,6 +740,12 @@ type BrowserPoolUpdateParams struct {
// If provided, replaces the extension list. Empty array clears all
// previously-selected extensions. Omit this field to leave extensions unchanged.
Extensions []shared.BrowserExtensionParam `json:"extensions,omitzero"`
+ // If provided, replaces the pool's network configuration. Omit to leave the
+ // existing configuration unchanged; an empty object ({}) removes it, while
+ // network: {private_hosts: []} sets an explicit empty list. Only applied to
+ // browsers created in the pool after the update; browsers already in the pool keep
+ // their configuration until discarded (see discard_all_idle).
+ Network BrowserNetworkConfigParam `json:"network,omitzero"`
// Profile configuration for browsers in a pool. Provide either id or name.
// Profiles must be created beforehand. Unlike single browser sessions, pools load
// the profile read-only and never persist changes back to it, so save_changes is
@@ -848,6 +906,10 @@ type BrowserPoolListParams struct {
// Case-insensitive substring match against browser pool name. IDs match by exact
// value.
Query param.Opt[string] `query:"query,omitzero" json:"-"`
+ // Filter pools by geographic region. Omit to list pools in all regions.
+ //
+ // Any of "us-east", "eu-west".
+ Region BrowserPoolListParamsRegion `query:"region,omitzero" json:"-"`
paramObj
}
@@ -859,6 +921,14 @@ func (r BrowserPoolListParams) URLQuery() (v url.Values, err error) {
})
}
+// Filter pools by geographic region. Omit to list pools in all regions.
+type BrowserPoolListParamsRegion string
+
+const (
+ BrowserPoolListParamsRegionUsEast BrowserPoolListParamsRegion = "us-east"
+ BrowserPoolListParamsRegionEuWest BrowserPoolListParamsRegion = "eu-west"
+)
+
type BrowserPoolDeleteParams struct {
// If true, force delete even if browsers are currently leased. Leased browsers
// will be terminated.
diff --git a/browserpool_test.go b/browserpool_test.go
index 2cbde78..2d20137 100644
--- a/browserpool_test.go
+++ b/browserpool_test.go
@@ -40,12 +40,16 @@ func TestBrowserPoolNewWithOptionalParams(t *testing.T) {
Headless: kernel.Bool(false),
KioskMode: kernel.Bool(true),
Name: kernel.String("my-pool"),
+ Network: kernel.BrowserNetworkConfigParam{
+ PrivateHosts: []string{"*.example.ts.net", "100.64.0.0/10"},
+ },
Profile: kernel.BrowserPoolNewParamsProfile{
ID: kernel.String("id"),
Name: kernel.String("name"),
},
ProxyID: kernel.String("proxy_id"),
RefreshOnProfileUpdate: kernel.Bool(true),
+ Region: kernel.BrowserPoolNewParamsRegionUsEast,
StartURL: kernel.String("https://example.com"),
Stealth: kernel.Bool(true),
Telemetry: kernel.BrowserPoolNewParamsTelemetry{
@@ -157,6 +161,9 @@ func TestBrowserPoolUpdateWithOptionalParams(t *testing.T) {
Headless: kernel.Bool(false),
KioskMode: kernel.Bool(true),
Name: kernel.String("my-pool"),
+ Network: kernel.BrowserNetworkConfigParam{
+ PrivateHosts: []string{"*.example.ts.net", "100.64.0.0/10"},
+ },
Profile: kernel.BrowserPoolUpdateParamsProfile{
ID: kernel.String("id"),
Name: kernel.String("name"),
@@ -242,6 +249,7 @@ func TestBrowserPoolListWithOptionalParams(t *testing.T) {
Name: kernel.String("name"),
Offset: kernel.Int(0),
Query: kernel.String("query"),
+ Region: kernel.BrowserPoolListParamsRegionUsEast,
})
if err != nil {
var apierr *kernel.Error
diff --git a/internal/version.go b/internal/version.go
index 0f12af4..c4c63b1 100644
--- a/internal/version.go
+++ b/internal/version.go
@@ -2,4 +2,4 @@
package internal
-const PackageVersion = "0.88.0" // x-release-please-version
+const PackageVersion = "0.89.0" // x-release-please-version
diff --git a/invocation.go b/invocation.go
index 12b708d..f6a8433 100644
--- a/invocation.go
+++ b/invocation.go
@@ -544,6 +544,10 @@ type InvocationListBrowsersResponseBrowser struct {
CreatedAt time.Time `json:"created_at" api:"required" format:"date-time"`
// Whether the browser session is running in headless mode.
Headless bool `json:"headless" api:"required"`
+ // Geographic region of the browser session. Fixed once the session is created.
+ //
+ // Any of "us-east", "eu-west".
+ Region string `json:"region" api:"required"`
// Unique identifier for the browser session
SessionID string `json:"session_id" api:"required"`
// Whether the browser session is running in stealth mode.
@@ -570,6 +574,9 @@ type InvocationListBrowsersResponseBrowser struct {
KioskMode bool `json:"kiosk_mode"`
// Human-readable name of the browser session, if one was set at creation.
Name string `json:"name"`
+ // Network configuration the session was created with, if any. Omitted when the
+ // session has no network configuration.
+ Network BrowserNetworkConfig `json:"network"`
// Browser pool this session was acquired from, if any.
Pool BrowserPoolRef `json:"pool"`
// Browser profile metadata.
@@ -615,6 +622,7 @@ type InvocationListBrowsersResponseBrowser struct {
CdpWsURL respjson.Field
CreatedAt respjson.Field
Headless respjson.Field
+ Region respjson.Field
SessionID respjson.Field
Stealth respjson.Field
TimeoutSeconds respjson.Field
@@ -626,6 +634,7 @@ type InvocationListBrowsersResponseBrowser struct {
GPU respjson.Field
KioskMode respjson.Field
Name respjson.Field
+ Network respjson.Field
Pool respjson.Field
Profile respjson.Field
ProfileSaveChanges respjson.Field
diff --git a/organizationlimit.go b/organizationlimit.go
index b1d5859..40bd413 100644
--- a/organizationlimit.go
+++ b/organizationlimit.go
@@ -36,9 +36,7 @@ func NewOrganizationLimitService(opts ...option.RequestOption) (r OrganizationLi
return
}
-// Get the organization's concurrency limit — the maximum browsers running at once
-// across on-demand sessions and browser pool reservations — and the default
-// per-project concurrency cap applied to projects without an explicit override.
+// Get the organization's effective limits and managed auth usage.
func (r *OrganizationLimitService) Get(ctx context.Context, opts ...option.RequestOption) (res *OrgLimits, err error) {
opts = slices.Concat(r.Options, opts)
path := "org/limits"
@@ -57,6 +55,17 @@ func (r *OrganizationLimitService) Update(ctx context.Context, body Organization
}
type OrgLimits struct {
+ // The organization's current non-deleted managed auth connections, counted
+ // org-wide across every project. Compare against max_auth_connections to show
+ // remaining capacity before a create is rejected with 403 insufficient_plan.
+ AuthConnectionsUsed int64 `json:"auth_connections_used" api:"required"`
+ // Maximum managed auth connections the organization's plan allows. Null means
+ // unlimited. Counted org-wide, so it cannot be multiplied across projects.
+ MaxAuthConnections int64 `json:"max_auth_connections" api:"required"`
+ // Smallest health_check_interval the organization's plan accepts on a managed auth
+ // connection. Requests below this are rejected with 400. Existing connections
+ // stored below the floor are grandfathered until edited.
+ MinHealthCheckIntervalSeconds int64 `json:"min_health_check_interval_seconds" api:"required"`
// Default maximum concurrent browsers applied to every project that has no
// explicit per-project override. Null means no org-level default, so such projects
// are uncapped (only the org-wide limit applies). Applies to existing and newly
@@ -69,6 +78,9 @@ type OrgLimits struct {
MaxConcurrentSessions int64 `json:"max_concurrent_sessions"`
// JSON contains metadata for fields, check presence with [respjson.Field.Valid].
JSON struct {
+ AuthConnectionsUsed respjson.Field
+ MaxAuthConnections respjson.Field
+ MinHealthCheckIntervalSeconds respjson.Field
DefaultProjectMaxConcurrentSessions respjson.Field
MaxConcurrentSessions respjson.Field
ExtraFields map[string]respjson.Field
diff --git a/release-please-config.json b/release-please-config.json
index dccefac..9b1c2fb 100644
--- a/release-please-config.json
+++ b/release-please-config.json
@@ -10,6 +10,7 @@
"bump-minor-pre-major": true,
"bump-patch-for-minor-pre-major": false,
"pull-request-header": "Automated Release PR",
+ "pull-request-footer": "Merge this pull request with a merge commit. Merging creates the GitHub release and publishes the package.",
"pull-request-title-pattern": "release: ${version}",
"changelog-sections": [
{