diff --git a/.release-please-manifest.json b/.release-please-manifest.json index fed4b17..c3e01e1 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.52.0" + ".": "0.53.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 33f284f..7915e04 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 112 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-4ce09d1a7546ab36f578cb27d819187eeb90c580b11834c7ff7a375aa22f9a20.yml -openapi_spec_hash: 1043ab2d699f6c828680c3352cd4cece +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-a33e59aa1758ba51f13538838ecd70b0a23ed69739b3022e8c2ce0622e42b904.yml +openapi_spec_hash: c042d2f6880c927be09aa9fa79d7241e config_hash: 08d55086449943a8fec212b870061a3f diff --git a/CHANGELOG.md b/CHANGELOG.md index ec22edc..dc39612 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,34 @@ # Changelog +## 0.53.0 (2026-05-12) + +Full Changelog: [v0.52.0...v0.53.0](https://github.com/kernel/kernel-go-sdk/compare/v0.52.0...v0.53.0) + +### Features + +* Add 'switch' MFA option type for generic method-switcher links ([b6c0d17](https://github.com/kernel/kernel-go-sdk/commit/b6c0d174ffaf8f5a0788f8c16a2fded6b56add76)) +* Add opt-in record_session flag to managed auth ([5673b41](https://github.com/kernel/kernel-go-sdk/commit/5673b41f0cc5d6233dbaf9875bc20b84b7cc7762)) +* **api:** server-side search on GET /projects ([81a050d](https://github.com/kernel/kernel-go-sdk/commit/81a050d64651caf4fefc58ce25a13c65ffe05bea)) +* browser_pools: add start_url config (KERNEL-1217 PR 2) ([b2c8f95](https://github.com/kernel/kernel-go-sdk/commit/b2c8f951ddde27daf1285b78a1ce6a7fc01d74ef)) +* managed-auth: surface awaiting_external_action even when fallback actions exist ([c1a9ba4](https://github.com/kernel/kernel-go-sdk/commit/c1a9ba4038b690875a5c8c5198d20671e27acec4)) +* Scope name uniqueness to project for profiles, session_pools, extensions, credentials ([be3c6bd](https://github.com/kernel/kernel-go-sdk/commit/be3c6bd46f8826cf5d17523bb1aa79c77d9a8d39)) + + +### Bug Fixes + +* **go:** avoid panic when http.DefaultTransport is wrapped ([15e7a88](https://github.com/kernel/kernel-go-sdk/commit/15e7a8860af570cb968ed0153742c86af85c9909)) + + +### Chores + +* avoid embedding reflect.Type for dead code elimination ([c267709](https://github.com/kernel/kernel-go-sdk/commit/c2677092b3f611e3470f5b6730f32fe896e2d688)) +* redact api-key headers in debug logs ([405d242](https://github.com/kernel/kernel-go-sdk/commit/405d242dd4564cb133cc036b7693169df2537e3b)) + + +### Documentation + +* clarify record_session description in OpenAPI spec ([c61abfa](https://github.com/kernel/kernel-go-sdk/commit/c61abfa3c5aa1d355bce51c10d08ad962e08424e)) + ## 0.52.0 (2026-04-29) Full Changelog: [v0.51.0...v0.52.0](https://github.com/kernel/kernel-go-sdk/compare/v0.51.0...v0.52.0) diff --git a/README.md b/README.md index 45fd7c1..3c30013 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.52.0' +go get -u 'github.com/kernel/kernel-go-sdk@v0.53.0' ``` diff --git a/authconnection.go b/authconnection.go index 4ae9e4d..fd9fa05 100644 --- a/authconnection.go +++ b/authconnection.go @@ -221,6 +221,9 @@ type ManagedAuth struct { Domain string `json:"domain" api:"required"` // Name of the profile associated with this auth connection ProfileName string `json:"profile_name" api:"required"` + // Whether to record browser session replays for this connection by default. Useful + // for debugging login flows. Can be overridden per-login. + RecordSession bool `json:"record_session" api:"required"` // Whether credentials are saved after every successful login. One-time codes // (TOTP, SMS, etc.) are not saved. SaveCredentials bool `json:"save_credentials" api:"required"` @@ -261,7 +264,8 @@ type ManagedAuth struct { // - { provider, path } for external provider item // - { provider, auto: true } for external provider domain lookup Credential ManagedAuthCredential `json:"credential"` - // Fields awaiting input (present when flow_step=awaiting_input) + // Fields awaiting input (present when flow_step=awaiting_input; may also be + // present with awaiting_external_action as fallback actions) DiscoveredFields []ManagedAuthDiscoveredField `json:"discovered_fields" api:"nullable"` // Machine-readable error code (present when flow_status=failed) ErrorCode string `json:"error_code" api:"nullable"` @@ -312,17 +316,19 @@ type ManagedAuth struct { LiveViewURL string `json:"live_view_url" api:"nullable" format:"uri"` // Optional login page URL to skip discovery LoginURL string `json:"login_url" format:"uri"` - // MFA method options (present when flow_step=awaiting_input and MFA selection - // required) + // MFA method options (present when flow_step=awaiting_input; may also be present + // with awaiting_external_action as fallback actions) MfaOptions []ManagedAuthMfaOption `json:"mfa_options" api:"nullable"` - // SSO buttons available (present when flow_step=awaiting_input) + // SSO buttons available (present when flow_step=awaiting_input; may also be + // present with awaiting_external_action as fallback actions) PendingSSOButtons []ManagedAuthPendingSSOButton `json:"pending_sso_buttons" api:"nullable"` // URL where the browser landed after successful login PostLoginURL string `json:"post_login_url" format:"uri"` // ID of the proxy associated with this connection, if any. ProxyID string `json:"proxy_id"` // Non-MFA choices presented during the auth flow, such as account selection or org - // pickers (present when flow_step=awaiting_input). + // pickers (present when flow_step=awaiting_input; may also be present with + // awaiting_external_action as fallback actions). SignInOptions []ManagedAuthSignInOption `json:"sign_in_options" api:"nullable"` // SSO provider being used (e.g., google, github, microsoft) SSOProvider string `json:"sso_provider" api:"nullable"` @@ -334,6 +340,7 @@ type ManagedAuth struct { ID respjson.Field Domain respjson.Field ProfileName respjson.Field + RecordSession respjson.Field SaveCredentials respjson.Field Status respjson.Field AllowedDomains respjson.Field @@ -430,7 +437,7 @@ type ManagedAuthDiscoveredField struct { // If this field is associated with an MFA option, the type of that option (e.g., // password field linked to "Enter password" option) // - // Any of "sms", "call", "email", "totp", "push", "password". + // Any of "sms", "call", "email", "totp", "push", "password", "switch". LinkedMfaType string `json:"linked_mfa_type" api:"nullable"` // Field placeholder Placeholder string `json:"placeholder"` @@ -491,9 +498,11 @@ const ( type ManagedAuthMfaOption struct { // The visible option text Label string `json:"label" api:"required"` - // The MFA delivery method type (includes password for auth method selection pages) + // The MFA delivery method type. Includes 'password' for auth method selection + // pages and 'switch' for generic method-switcher links like "Use another method" + // that do not name a specific method. // - // Any of "sms", "call", "email", "totp", "push", "password". + // Any of "sms", "call", "email", "totp", "push", "password", "switch". Type string `json:"type" api:"required"` // Additional instructions from the site Description string `json:"description" api:"nullable"` @@ -582,6 +591,9 @@ type ManagedAuthCreateRequestParam struct { HealthCheckInterval param.Opt[int64] `json:"health_check_interval,omitzero"` // Optional login page URL to skip discovery LoginURL param.Opt[string] `json:"login_url,omitzero" format:"uri"` + // Whether to record browser sessions for this connection by default. Useful for + // debugging. Can be overridden per-login. Defaults to false. + RecordSession param.Opt[bool] `json:"record_session,omitzero"` // Whether to save credentials after every successful login. Defaults to true. // One-time codes (TOTP, SMS, etc.) are not saved. SaveCredentials param.Opt[bool] `json:"save_credentials,omitzero"` @@ -672,6 +684,8 @@ type ManagedAuthUpdateRequestParam struct { HealthCheckInterval param.Opt[int64] `json:"health_check_interval,omitzero"` // Login page URL. Set to empty string to clear. LoginURL param.Opt[string] `json:"login_url,omitzero" format:"uri"` + // Whether to record browser sessions for this connection by default + RecordSession param.Opt[bool] `json:"record_session,omitzero"` // Whether to save credentials after every successful login SaveCredentials param.Opt[bool] `json:"save_credentials,omitzero"` // Additional domains valid for this auth flow (replaces existing list) @@ -915,7 +929,8 @@ type AuthConnectionFollowResponseManagedAuthState struct { FlowStep string `json:"flow_step" api:"required"` // Time the state was reported. Timestamp time.Time `json:"timestamp" api:"required" format:"date-time"` - // Fields awaiting input (present when flow_step=AWAITING_INPUT). + // Fields awaiting input (present when flow_step=AWAITING_INPUT; may also be + // present with AWAITING_EXTERNAL_ACTION as fallback actions). DiscoveredFields []AuthConnectionFollowResponseManagedAuthStateDiscoveredField `json:"discovered_fields"` // Machine-readable error code (present when flow_status=FAILED). ErrorCode string `json:"error_code"` @@ -932,15 +947,17 @@ type AuthConnectionFollowResponseManagedAuthState struct { HostedURL string `json:"hosted_url" format:"uri"` // Browser live view URL for debugging. LiveViewURL string `json:"live_view_url" format:"uri"` - // MFA method options (present when flow_step=AWAITING_INPUT and MFA selection - // required). + // MFA method options (present when flow_step=AWAITING_INPUT; may also be present + // with AWAITING_EXTERNAL_ACTION as fallback actions). MfaOptions []AuthConnectionFollowResponseManagedAuthStateMfaOption `json:"mfa_options"` - // SSO buttons available (present when flow_step=AWAITING_INPUT). + // SSO buttons available (present when flow_step=AWAITING_INPUT; may also be + // present with AWAITING_EXTERNAL_ACTION as fallback actions). PendingSSOButtons []AuthConnectionFollowResponseManagedAuthStatePendingSSOButton `json:"pending_sso_buttons"` // URL where the browser landed after successful login. PostLoginURL string `json:"post_login_url" format:"uri"` // Non-MFA choices presented during the auth flow, such as account selection or org - // pickers (present when flow_step=AWAITING_INPUT). + // pickers (present when flow_step=AWAITING_INPUT; may also be present with + // AWAITING_EXTERNAL_ACTION as fallback actions). SignInOptions []AuthConnectionFollowResponseManagedAuthStateSignInOption `json:"sign_in_options"` // Visible error message from the website (e.g., 'Incorrect password'). Present // when the website displays an error during login. @@ -992,7 +1009,7 @@ type AuthConnectionFollowResponseManagedAuthStateDiscoveredField struct { // If this field is associated with an MFA option, the type of that option (e.g., // password field linked to "Enter password" option) // - // Any of "sms", "call", "email", "totp", "push", "password". + // Any of "sms", "call", "email", "totp", "push", "password", "switch". LinkedMfaType string `json:"linked_mfa_type" api:"nullable"` // Field placeholder Placeholder string `json:"placeholder"` @@ -1025,9 +1042,11 @@ func (r *AuthConnectionFollowResponseManagedAuthStateDiscoveredField) UnmarshalJ type AuthConnectionFollowResponseManagedAuthStateMfaOption struct { // The visible option text Label string `json:"label" api:"required"` - // The MFA delivery method type (includes password for auth method selection pages) + // The MFA delivery method type. Includes 'password' for auth method selection + // pages and 'switch' for generic method-switcher links like "Use another method" + // that do not name a specific method. // - // Any of "sms", "call", "email", "totp", "push", "password". + // Any of "sms", "call", "email", "totp", "push", "password", "switch". Type string `json:"type" api:"required"` // Additional instructions from the site Description string `json:"description" api:"nullable"` @@ -1149,6 +1168,9 @@ func (r AuthConnectionListParams) URLQuery() (v url.Values, err error) { } type AuthConnectionLoginParams struct { + // Override the connection's default for recording this login's browser session. + // When omitted, the connection's record_session default is used. + RecordSession param.Opt[bool] `json:"record_session,omitzero"` // Proxy selection. Provide either id or name. The proxy must belong to the // caller's org. Proxy AuthConnectionLoginParamsProxy `json:"proxy,omitzero"` diff --git a/authconnection_test.go b/authconnection_test.go index f83e246..f54dc72 100644 --- a/authconnection_test.go +++ b/authconnection_test.go @@ -43,6 +43,7 @@ func TestAuthConnectionNewWithOptionalParams(t *testing.T) { ID: kernel.String("id"), Name: kernel.String("name"), }, + RecordSession: kernel.Bool(false), SaveCredentials: kernel.Bool(true), }, }) @@ -109,6 +110,7 @@ func TestAuthConnectionUpdateWithOptionalParams(t *testing.T) { ID: kernel.String("id"), Name: kernel.String("name"), }, + RecordSession: kernel.Bool(false), SaveCredentials: kernel.Bool(true), }, }, @@ -194,6 +196,7 @@ func TestAuthConnectionLoginWithOptionalParams(t *testing.T) { ID: kernel.String("id"), Name: kernel.String("name"), }, + RecordSession: kernel.Bool(true), }, ) if err != nil { diff --git a/browser.go b/browser.go index 4533fc1..7a753a1 100644 --- a/browser.go +++ b/browser.go @@ -328,6 +328,9 @@ type BrowserNewResponse struct { Profile Profile `json:"profile"` // ID of the proxy associated with this browser session, if any. ProxyID string `json:"proxy_id"` + // URL the session was asked to navigate to on creation, if any. Recorded for + // debugging — navigation is best-effort and may have failed. + StartURL string `json:"start_url"` // Session usage metrics. Usage BrowserUsage `json:"usage"` // Initial browser window size in pixels with optional refresh rate. If omitted, @@ -361,6 +364,7 @@ type BrowserNewResponse struct { Pool respjson.Field Profile respjson.Field ProxyID respjson.Field + StartURL respjson.Field Usage respjson.Field Viewport respjson.Field ExtraFields map[string]respjson.Field @@ -411,6 +415,9 @@ type BrowserGetResponse struct { Profile Profile `json:"profile"` // ID of the proxy associated with this browser session, if any. ProxyID string `json:"proxy_id"` + // URL the session was asked to navigate to on creation, if any. Recorded for + // debugging — navigation is best-effort and may have failed. + StartURL string `json:"start_url"` // Session usage metrics. Usage BrowserUsage `json:"usage"` // Initial browser window size in pixels with optional refresh rate. If omitted, @@ -444,6 +451,7 @@ type BrowserGetResponse struct { Pool respjson.Field Profile respjson.Field ProxyID respjson.Field + StartURL respjson.Field Usage respjson.Field Viewport respjson.Field ExtraFields map[string]respjson.Field @@ -494,6 +502,9 @@ type BrowserUpdateResponse struct { Profile Profile `json:"profile"` // ID of the proxy associated with this browser session, if any. ProxyID string `json:"proxy_id"` + // URL the session was asked to navigate to on creation, if any. Recorded for + // debugging — navigation is best-effort and may have failed. + StartURL string `json:"start_url"` // Session usage metrics. Usage BrowserUsage `json:"usage"` // Initial browser window size in pixels with optional refresh rate. If omitted, @@ -527,6 +538,7 @@ type BrowserUpdateResponse struct { Pool respjson.Field Profile respjson.Field ProxyID respjson.Field + StartURL respjson.Field Usage respjson.Field Viewport respjson.Field ExtraFields map[string]respjson.Field @@ -577,6 +589,9 @@ type BrowserListResponse struct { Profile Profile `json:"profile"` // ID of the proxy associated with this browser session, if any. ProxyID string `json:"proxy_id"` + // URL the session was asked to navigate to on creation, if any. Recorded for + // debugging — navigation is best-effort and may have failed. + StartURL string `json:"start_url"` // Session usage metrics. Usage BrowserUsage `json:"usage"` // Initial browser window size in pixels with optional refresh rate. If omitted, @@ -610,6 +625,7 @@ type BrowserListResponse struct { Pool respjson.Field Profile respjson.Field ProxyID respjson.Field + StartURL respjson.Field Usage respjson.Field Viewport respjson.Field ExtraFields map[string]respjson.Field @@ -665,6 +681,12 @@ type BrowserNewParams struct { // Optional proxy to associate to the browser session. Must reference a proxy // belonging to the caller's org. ProxyID param.Opt[string] `json:"proxy_id,omitzero"` + // Optional URL to navigate to immediately after the browser is created. + // Best-effort: failures to navigate do not fail browser creation. Any pre-existing + // tabs are reduced to a single tab which is then navigated. Accepts any URL + // Chromium can resolve, including chrome:// pages. Ignored when reusing an + // existing persistent session. + StartURL param.Opt[string] `json:"start_url,omitzero"` // If true, launches the browser in stealth mode to reduce detection by anti-bot // mechanisms. Stealth param.Opt[bool] `json:"stealth,omitzero"` diff --git a/browser_test.go b/browser_test.go index c8681a6..b4d0090 100644 --- a/browser_test.go +++ b/browser_test.go @@ -47,6 +47,7 @@ func TestBrowserNewWithOptionalParams(t *testing.T) { SaveChanges: kernel.Bool(true), }, ProxyID: kernel.String("proxy_id"), + StartURL: kernel.String("https://example.com"), Stealth: kernel.Bool(true), TimeoutSeconds: kernel.Int(10), Viewport: shared.BrowserViewportParam{ diff --git a/browserpool.go b/browserpool.go index 3069e49..d3558d2 100644 --- a/browserpool.go +++ b/browserpool.go @@ -187,7 +187,7 @@ type BrowserPoolBrowserPoolConfig struct { // If true, launches the browser in kiosk mode to hide address bar and tabs in live // view. KioskMode bool `json:"kiosk_mode"` - // Optional name for the browser pool. Must be unique within the organization. + // Optional name for the browser pool. Must be unique within the project. Name string `json:"name"` // Profile selection for the browser session. Provide either id or name. If // specified, the matching profile will be loaded into the browser session. @@ -196,6 +196,12 @@ type BrowserPoolBrowserPoolConfig struct { // Optional proxy to associate to the browser session. Must reference a proxy // belonging to the caller's org. ProxyID string `json:"proxy_id"` + // Optional URL to navigate to when a new browser is warmed into the pool. + // Best-effort: failures to navigate do not fail pool fill. Only applied to + // newly-warmed browsers — browsers reused via release/acquire keep whatever URL + // the previous lease left them on. Accepts any URL Chromium can resolve, including + // chrome:// pages. + StartURL string `json:"start_url"` // If true, launches the browser in stealth mode to reduce detection by anti-bot // mechanisms. Stealth bool `json:"stealth"` @@ -226,6 +232,7 @@ type BrowserPoolBrowserPoolConfig struct { Name respjson.Field Profile respjson.Field ProxyID respjson.Field + StartURL respjson.Field Stealth respjson.Field TimeoutSeconds respjson.Field Viewport respjson.Field @@ -277,6 +284,9 @@ type BrowserPoolAcquireResponse struct { Profile Profile `json:"profile"` // ID of the proxy associated with this browser session, if any. ProxyID string `json:"proxy_id"` + // URL the session was asked to navigate to on creation, if any. Recorded for + // debugging — navigation is best-effort and may have failed. + StartURL string `json:"start_url"` // Session usage metrics. Usage BrowserUsage `json:"usage"` // Initial browser window size in pixels with optional refresh rate. If omitted, @@ -310,6 +320,7 @@ type BrowserPoolAcquireResponse struct { Pool respjson.Field Profile respjson.Field ProxyID respjson.Field + StartURL respjson.Field Usage respjson.Field Viewport respjson.Field ExtraFields map[string]respjson.Field @@ -335,11 +346,17 @@ type BrowserPoolNewParams struct { // If true, launches the browser in kiosk mode to hide address bar and tabs in live // view. KioskMode param.Opt[bool] `json:"kiosk_mode,omitzero"` - // Optional name for the browser pool. Must be unique within the organization. + // Optional name for the browser pool. Must be unique within the project. Name param.Opt[string] `json:"name,omitzero"` // Optional proxy to associate to the browser session. Must reference a proxy // belonging to the caller's org. ProxyID param.Opt[string] `json:"proxy_id,omitzero"` + // Optional URL to navigate to when a new browser is warmed into the pool. + // Best-effort: failures to navigate do not fail pool fill. Only applied to + // newly-warmed browsers — browsers reused via release/acquire keep whatever URL + // the previous lease left them on. Accepts any URL Chromium can resolve, including + // chrome:// pages. + StartURL param.Opt[string] `json:"start_url,omitzero"` // If true, launches the browser in stealth mode to reduce detection by anti-bot // mechanisms. Stealth param.Opt[bool] `json:"stealth,omitzero"` @@ -396,11 +413,17 @@ type BrowserPoolUpdateParams struct { // If true, launches the browser in kiosk mode to hide address bar and tabs in live // view. KioskMode param.Opt[bool] `json:"kiosk_mode,omitzero"` - // Optional name for the browser pool. Must be unique within the organization. + // Optional name for the browser pool. Must be unique within the project. Name param.Opt[string] `json:"name,omitzero"` // Optional proxy to associate to the browser session. Must reference a proxy // belonging to the caller's org. ProxyID param.Opt[string] `json:"proxy_id,omitzero"` + // Optional URL to navigate to when a new browser is warmed into the pool. + // Best-effort: failures to navigate do not fail pool fill. Only applied to + // newly-warmed browsers — browsers reused via release/acquire keep whatever URL + // the previous lease left them on. Accepts any URL Chromium can resolve, including + // chrome:// pages. + StartURL param.Opt[string] `json:"start_url,omitzero"` // If true, launches the browser in stealth mode to reduce detection by anti-bot // mechanisms. Stealth param.Opt[bool] `json:"stealth,omitzero"` diff --git a/browserpool_test.go b/browserpool_test.go index fd59d64..e73ba4e 100644 --- a/browserpool_test.go +++ b/browserpool_test.go @@ -46,6 +46,7 @@ func TestBrowserPoolNewWithOptionalParams(t *testing.T) { SaveChanges: kernel.Bool(true), }, ProxyID: kernel.String("proxy_id"), + StartURL: kernel.String("https://example.com"), Stealth: kernel.Bool(true), TimeoutSeconds: kernel.Int(60), Viewport: shared.BrowserViewportParam{ @@ -122,6 +123,7 @@ func TestBrowserPoolUpdateWithOptionalParams(t *testing.T) { SaveChanges: kernel.Bool(true), }, ProxyID: kernel.String("proxy_id"), + StartURL: kernel.String("https://example.com"), Stealth: kernel.Bool(true), TimeoutSeconds: kernel.Int(60), Viewport: shared.BrowserViewportParam{ diff --git a/credential.go b/credential.go index fd8befe..5980b80 100644 --- a/credential.go +++ b/credential.go @@ -133,7 +133,7 @@ func (r *CredentialService) TotpCode(ctx context.Context, idOrName string, opts type CreateCredentialRequestParam struct { // Target domain this credential is for Domain string `json:"domain" api:"required"` - // Unique name for the credential within the organization + // Unique name for the credential within the project Name string `json:"name" api:"required"` // Field name to value mapping (e.g., username, password) Values map[string]string `json:"values,omitzero" api:"required"` @@ -164,7 +164,7 @@ type Credential struct { CreatedAt time.Time `json:"created_at" api:"required" format:"date-time"` // Target domain this credential is for Domain string `json:"domain" api:"required"` - // Unique name for the credential within the organization + // Unique name for the credential within the project Name string `json:"name" api:"required"` // When the credential was last updated UpdatedAt time.Time `json:"updated_at" api:"required" format:"date-time"` diff --git a/default_http_client.go b/default_http_client.go index 036bc47..f0f2b97 100644 --- a/default_http_client.go +++ b/default_http_client.go @@ -14,11 +14,17 @@ import ( const defaultResponseHeaderTimeout = 10 * time.Minute // defaultHTTPClient returns an [*http.Client] used when the caller does not -// supply one via [option.WithHTTPClient]. It clones [http.DefaultTransport] -// and adds a [http.Transport.ResponseHeaderTimeout] so stuck connections -// fail fast instead of compounding across retries. +// supply one via [option.WithHTTPClient]. When [http.DefaultTransport] is the +// stdlib [*http.Transport], it is cloned and a [http.Transport.ResponseHeaderTimeout] +// is set so stuck connections fail fast instead of compounding across retries. +// If [http.DefaultTransport] has been wrapped (for example by otelhttp for +// distributed tracing), the wrapping is preserved and the header timeout is +// skipped. func defaultHTTPClient() *http.Client { - transport := http.DefaultTransport.(*http.Transport).Clone() - transport.ResponseHeaderTimeout = defaultResponseHeaderTimeout - return &http.Client{Transport: transport} + if t, ok := http.DefaultTransport.(*http.Transport); ok { + t = t.Clone() + t.ResponseHeaderTimeout = defaultResponseHeaderTimeout + return &http.Client{Transport: t} + } + return &http.Client{Transport: http.DefaultTransport} } diff --git a/extension.go b/extension.go index 09304dd..9fcb4a3 100644 --- a/extension.go +++ b/extension.go @@ -108,7 +108,7 @@ type ExtensionListResponse struct { // Timestamp when the extension was last used LastUsedAt time.Time `json:"last_used_at" api:"nullable" format:"date-time"` // Optional, easier-to-reference name for the extension. Must be unique within the - // organization. + // project. Name string `json:"name" api:"nullable"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { @@ -139,7 +139,7 @@ type ExtensionUploadResponse struct { // Timestamp when the extension was last used LastUsedAt time.Time `json:"last_used_at" api:"nullable" format:"date-time"` // Optional, easier-to-reference name for the extension. Must be unique within the - // organization. + // project. Name string `json:"name" api:"nullable"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { @@ -190,7 +190,7 @@ const ( type ExtensionUploadParams struct { // ZIP file containing the browser extension. File io.Reader `json:"file,omitzero" api:"required" format:"binary"` - // Optional unique name within the organization to reference this extension. + // Optional unique name within the project to reference this extension. Name param.Opt[string] `json:"name,omitzero"` paramObj } diff --git a/internal/apiform/encoder.go b/internal/apiform/encoder.go index 5cf185c..1cd9ea9 100644 --- a/internal/apiform/encoder.go +++ b/internal/apiform/encoder.go @@ -58,7 +58,7 @@ type encoderField struct { } type encoderEntry struct { - reflect.Type + typ reflect.Type dateFormat string arrayFmt string root bool @@ -76,7 +76,7 @@ func (e *encoder) marshal(value any, writer *multipart.Writer) error { func (e *encoder) typeEncoder(t reflect.Type) encoderFunc { entry := encoderEntry{ - Type: t, + typ: t, dateFormat: e.dateFormat, arrayFmt: e.arrayFmt, root: e.root, diff --git a/internal/apijson/decoder.go b/internal/apijson/decoder.go index fe2c4a0..54280e8 100644 --- a/internal/apijson/decoder.go +++ b/internal/apijson/decoder.go @@ -80,7 +80,7 @@ type decoderField struct { } type decoderEntry struct { - reflect.Type + typ reflect.Type dateFormat string root bool } @@ -108,7 +108,7 @@ func (d *decoderBuilder) unmarshalWithExactness(raw []byte, to any) (exactness, func (d *decoderBuilder) typeDecoder(t reflect.Type) decoderFunc { entry := decoderEntry{ - Type: t, + typ: t, dateFormat: d.dateFormat, root: d.root, } diff --git a/internal/apijson/encoder.go b/internal/apijson/encoder.go index afe611e..9b9fe1f 100644 --- a/internal/apijson/encoder.go +++ b/internal/apijson/encoder.go @@ -46,7 +46,7 @@ type encoderField struct { } type encoderEntry struct { - reflect.Type + typ reflect.Type dateFormat string root bool } @@ -63,7 +63,7 @@ func (e *encoder) marshal(value any) ([]byte, error) { func (e *encoder) typeEncoder(t reflect.Type) encoderFunc { entry := encoderEntry{ - Type: t, + typ: t, dateFormat: e.dateFormat, root: e.root, } diff --git a/internal/apiquery/encoder.go b/internal/apiquery/encoder.go index 596fbb4..faadb43 100644 --- a/internal/apiquery/encoder.go +++ b/internal/apiquery/encoder.go @@ -29,7 +29,7 @@ type encoderField struct { } type encoderEntry struct { - reflect.Type + typ reflect.Type dateFormat string root bool settings QuerySettings @@ -42,7 +42,7 @@ type Pair struct { func (e *encoder) typeEncoder(t reflect.Type) encoderFunc { entry := encoderEntry{ - Type: t, + typ: t, dateFormat: e.dateFormat, root: e.root, settings: e.settings, diff --git a/internal/version.go b/internal/version.go index 6542caa..fd0f8e3 100644 --- a/internal/version.go +++ b/internal/version.go @@ -2,4 +2,4 @@ package internal -const PackageVersion = "0.52.0" // x-release-please-version +const PackageVersion = "0.53.0" // x-release-please-version diff --git a/invocation.go b/invocation.go index 872a775..190c3e5 100644 --- a/invocation.go +++ b/invocation.go @@ -574,6 +574,9 @@ type InvocationListBrowsersResponseBrowser struct { Profile Profile `json:"profile"` // ID of the proxy associated with this browser session, if any. ProxyID string `json:"proxy_id"` + // URL the session was asked to navigate to on creation, if any. Recorded for + // debugging — navigation is best-effort and may have failed. + StartURL string `json:"start_url"` // Session usage metrics. Usage BrowserUsage `json:"usage"` // Initial browser window size in pixels with optional refresh rate. If omitted, @@ -607,6 +610,7 @@ type InvocationListBrowsersResponseBrowser struct { Pool respjson.Field Profile respjson.Field ProxyID respjson.Field + StartURL respjson.Field Usage respjson.Field Viewport respjson.Field ExtraFields map[string]respjson.Field diff --git a/option/middleware.go b/option/middleware.go index 8ec9dd6..4be0987 100644 --- a/option/middleware.go +++ b/option/middleware.go @@ -8,6 +8,10 @@ import ( "net/http/httputil" ) +// sensitiveLogHeaders are redacted before request and response content is +// written to the debug logger. +var sensitiveLogHeaders = []string{"authorization", "api-key", "x-api-key", "cookie", "set-cookie"} + // WithDebugLog logs the HTTP request and response content. // If the logger parameter is nil, it uses the default logger. // @@ -20,7 +24,7 @@ func WithDebugLog(logger *log.Logger) RequestOption { logger = log.Default() } - if reqBytes, err := httputil.DumpRequest(req, true); err == nil { + if reqBytes, err := dumpRedactedRequest(req); err == nil { logger.Printf("Request Content:\n%s\n", reqBytes) } @@ -29,10 +33,48 @@ func WithDebugLog(logger *log.Logger) RequestOption { return resp, err } - if respBytes, err := httputil.DumpResponse(resp, true); err == nil { + if respBytes, err := dumpRedactedResponse(resp); err == nil { logger.Printf("Response Content:\n%s\n", respBytes) } return resp, err }) } + +// dumpRedactedRequest dumps req with sensitive headers replaced. The +// original headers are restored via defer so a panic in DumpRequest cannot +// leak the placeholder map into the live request sent downstream. +func dumpRedactedRequest(req *http.Request) ([]byte, error) { + origHeaders := req.Header + req.Header = redactDebugHeaders(origHeaders) + defer func() { req.Header = origHeaders }() + return httputil.DumpRequest(req, true) +} + +func dumpRedactedResponse(resp *http.Response) ([]byte, error) { + origHeaders := resp.Header + resp.Header = redactDebugHeaders(origHeaders) + defer func() { resp.Header = origHeaders }() + return httputil.DumpResponse(resp, true) +} + +func redactDebugHeaders(headers http.Header) http.Header { + var redacted http.Header + for _, name := range sensitiveLogHeaders { + values := headers.Values(name) + if len(values) == 0 { + continue + } + if redacted == nil { + redacted = headers.Clone() + } + redacted.Del(name) + for range values { + redacted.Add(name, "***") + } + } + if redacted == nil { + return headers + } + return redacted +} diff --git a/profile.go b/profile.go index ac7b022..58304a9 100644 --- a/profile.go +++ b/profile.go @@ -110,7 +110,7 @@ func (r *ProfileService) Download(ctx context.Context, idOrName string, opts ... } type ProfileNewParams struct { - // Optional name of the profile. Must be unique within the organization. + // Optional name of the profile. Must be unique within the project. Name param.Opt[string] `json:"name,omitzero"` paramObj } diff --git a/project.go b/project.go index acf1f22..f2bdb23 100644 --- a/project.go +++ b/project.go @@ -223,6 +223,8 @@ type ProjectListParams struct { Limit param.Opt[int64] `query:"limit,omitzero" json:"-"` // Number of results to skip Offset param.Opt[int64] `query:"offset,omitzero" json:"-"` + // Case-insensitive substring match against project name + Query param.Opt[string] `query:"query,omitzero" json:"-"` paramObj } diff --git a/project_test.go b/project_test.go index 047ac95..a8fefba 100644 --- a/project_test.go +++ b/project_test.go @@ -111,6 +111,7 @@ func TestProjectListWithOptionalParams(t *testing.T) { _, err := client.Projects.List(context.TODO(), kernel.ProjectListParams{ Limit: kernel.Int(100), Offset: kernel.Int(0), + Query: kernel.String("query"), }) if err != nil { var apierr *kernel.Error