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": [ {