From 57bb74744e160dc8a456288094d72fc65b4c9cf6 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Mon, 27 Jul 2026 20:52:53 +0000 Subject: [PATCH 1/4] chore: update Go SDK to v0.84.0 and surface telemetry capture in auth timeline Updates github.com/kernel/kernel-go-sdk to 65f3b913775353974858c92a6164ae25232ecc81 (v0.84.0). A full enumeration of api.md methods against the CLI service interfaces found no missing commands: all 128 SDK methods are covered (the one x-cli-skip endpoint, POST /auth/connections/{id}/exchange, is correctly absent). The only API surface change in this SDK bump is a new response field, ManagedAuthTimelineEvent. TelemetryCaptured, so no new flags were needed. Surfaces that field as a "Telemetry" column in `kernel auth connections timeline`, so users can tell which timeline events have browser telemetry available via `kernel browsers telemetry events`. The column shows "-" when the event has no browser session or when the API does not report the field, and yes/no otherwise. JSON output already passes the field through verbatim. Tested against the live API: - kernel auth connections timeline --per-page 5 (three connections) - kernel auth connections timeline --per-page 2 --output json - kernel auth connections list, kernel browsers list, kernel profiles list - go build ./... and go test ./... pass Co-Authored-By: Claude Opus 5 --- cmd/auth_connections.go | 10 +++++++++- cmd/auth_connections_test.go | 15 ++++++++++++++- go.mod | 2 +- go.sum | 4 ++-- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/cmd/auth_connections.go b/cmd/auth_connections.go index 984315a..87f47d4 100644 --- a/cmd/auth_connections.go +++ b/cmd/auth_connections.go @@ -839,7 +839,7 @@ func (c AuthConnectionCmd) Timeline(ctx context.Context, in AuthConnectionTimeli return nil } - tableData := pterm.TableData{{"Timestamp", "Type", "Status", "Step", "Browser Session", "Details"}} + tableData := pterm.TableData{{"Timestamp", "Type", "Status", "Step", "Browser Session", "Telemetry", "Details"}} for _, e := range events { details := e.ErrorMessage if details == "" { @@ -848,12 +848,20 @@ func (c AuthConnectionCmd) Timeline(ctx context.Context, in AuthConnectionTimeli if details == "" && e.PreviousStatus != "" { details = fmt.Sprintf("%s -> %s", e.PreviousStatus, e.Status) } + // Telemetry only means something when the event has a browser session + // whose events `kernel browsers telemetry` could fetch, and when the API + // actually reported the field. + telemetry := "-" + if e.BrowserSessionID != "" && e.JSON.TelemetryCaptured.Valid() { + telemetry = lo.Ternary(e.TelemetryCaptured, "yes", "no") + } tableData = append(tableData, []string{ util.FormatLocal(e.Timestamp), string(e.Type), string(e.Status), string(e.Step), util.OrDash(e.BrowserSessionID), + telemetry, util.OrDash(details), }) } diff --git a/cmd/auth_connections_test.go b/cmd/auth_connections_test.go index edd392e..4b9bca9 100644 --- a/cmd/auth_connections_test.go +++ b/cmd/auth_connections_test.go @@ -690,13 +690,23 @@ func TestSubmit_CanonicalAndLegacyAreMutuallyExclusive(t *testing.T) { func TestTimeline_RendersEventsAndPagination(t *testing.T) { buf := capturePtermOutput(t) var captured kernel.AuthConnectionTimelineParams + // Decoded from JSON so the telemetry_captured field registers as present; + // the CLI distinguishes "reported false" from "not reported at all". + var loginEvent kernel.ManagedAuthTimelineEvent + require.NoError(t, json.Unmarshal([]byte(`{ + "id": "e1", + "type": "login", + "status": "SUCCESS", + "browser_session_id": "browser_1", + "telemetry_captured": true + }`), &loginEvent)) fake := &FakeAuthConnectionService{ TimelineFunc: func(ctx context.Context, id string, query kernel.AuthConnectionTimelineParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.ManagedAuthTimelineEvent], error) { captured = query // Return perPage+1 events so the CLI reports another page exists. return &pagination.OffsetPagination[kernel.ManagedAuthTimelineEvent]{ Items: []kernel.ManagedAuthTimelineEvent{ - {ID: "e1", Type: "login", Status: "SUCCESS", BrowserSessionID: "browser_1"}, + loginEvent, {ID: "e2", Type: "reauth", Status: "FAILED", ErrorMessage: "boom"}, {ID: "e3", Type: "health_check", Status: "SUCCESS"}, }, @@ -714,6 +724,9 @@ func TestTimeline_RendersEventsAndPagination(t *testing.T) { out := buf.String() assert.Contains(t, out, "browser_1") assert.Contains(t, out, "boom") + // Telemetry capture is reported for events that have a browser session. + assert.Contains(t, out, "Telemetry") + assert.Regexp(t, `browser_1.*yes`, out) // The third event is truncated off the page. assert.NotContains(t, out, "health_check") assert.Contains(t, out, "Has more: yes") diff --git a/go.mod b/go.mod index f125d5c..3d90ae6 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/joho/godotenv v1.5.1 - github.com/kernel/kernel-go-sdk v0.83.1-0.20260724213320-12b3ec62f63e + github.com/kernel/kernel-go-sdk v0.84.0 github.com/klauspost/compress v1.18.5 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 diff --git a/go.sum b/go.sum index b7e971f..9341ef3 100644 --- a/go.sum +++ b/go.sum @@ -64,8 +64,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kernel/kernel-go-sdk v0.83.1-0.20260724213320-12b3ec62f63e h1:Pe1fRnIM4Zf7ddVg/wd+qTD0X4t2NmXRIeXyY98AILI= -github.com/kernel/kernel-go-sdk v0.83.1-0.20260724213320-12b3ec62f63e/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.84.0 h1:EYJC6TMFDiosqsbWbCLlVkVr/B8/hTYfyVAUinb7Hsk= +github.com/kernel/kernel-go-sdk v0.84.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= From 7ec80fc141931db75b2fbcb3a6dd18004775e613 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:05:33 +0000 Subject: [PATCH 2/4] CLI: Update Go SDK to v0.85.0 and add proxy CA bundle support Updates github.com/kernel/kernel-go-sdk to e6dff4a41334b0008f7d0469bf83477905cdc589 (v0.85.0). Full enumeration of all 127 SDK methods in api.md against the CLI command tree found no missing commands. The only coverage gap was a new param field on ProxyNewParamsConfigCustom. New flags on `kernel proxies create`: - --ca-bundle: inline PEM-encoded CA certificate bundle - --ca-bundle-file: read the bundle from a file ('-' for stdin) The two are mutually exclusive; the bundle is rejected early if it is not PEM-encoded, and warned about (ignored) for non-custom proxy types. Surfaces the new has_ca_bundle response field: - `proxies get` / `proxies check`: "Has CA Bundle" row - `proxies list`: "CA bundle" marker in the Config column - `proxies create`: confirms the write-only bundle was stored Tested: - Real API: `proxies create --type custom --ca-bundle-file` reaches server-side ca_bundle validation ("ca_bundle must be 65536 bytes or less, got 80569" for an oversized bundle; a valid bundle passes that check and only fails the live proxy connection test, as no reachable MITM proxy is available from CI). No resources leaked. - Local mock API: verified the wire body carries the full PEM as `ca_bundle` for inline, file, and stdin inputs, and that has_ca_bundle renders in create/get/list/check output. - Flag errors: mutual exclusion, non-PEM input, missing file, non-custom warning. - go build ./..., go vet ./..., go test ./... all pass (6 new unit tests). Triggered by: kernel/kernel-go-sdk@e6dff4a41334b0008f7d0469bf83477905cdc589 Co-Authored-By: Claude Opus 5 --- README.md | 3 + cmd/proxies/check.go | 5 ++ cmd/proxies/create.go | 84 ++++++++++++++++++++++----- cmd/proxies/create_test.go | 113 +++++++++++++++++++++++++++++++++++++ cmd/proxies/get.go | 5 ++ cmd/proxies/list.go | 6 +- cmd/proxies/proxies.go | 6 ++ cmd/proxies/types.go | 5 +- go.mod | 2 +- go.sum | 4 +- 10 files changed, 213 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 4dbc4d1..21c3ae8 100644 --- a/README.md +++ b/README.md @@ -873,6 +873,9 @@ kernel proxies create --type datacenter --country US --protocol http --name "US # Create a custom proxy kernel proxies create --type custom --host proxy.example.com --port 8080 --username myuser --password mypass --name "My Custom Proxy" +# Create a custom TLS-terminating (MITM) proxy, trusting its CA bundle in the browser +kernel proxies create --type custom --host proxy.example.com --port 8080 --ca-bundle-file ./mitm-ca.pem --name "MITM Proxy" + # Create a residential proxy with location and OS kernel proxies create --type residential --country US --city sanfrancisco --state CA --zip 94107 --asn AS15169 --os windows --name "SF Residential" diff --git a/cmd/proxies/check.go b/cmd/proxies/check.go index 6c6657e..7381446 100644 --- a/cmd/proxies/check.go +++ b/cmd/proxies/check.go @@ -148,6 +148,11 @@ func getProxyCheckConfigRows(proxy *kernel.ProxyCheckResponse) [][]string { hasPassword = "Yes" } rows = append(rows, []string{"Has Password", hasPassword}) + hasCaBundle := "No" + if config.HasCaBundle { + hasCaBundle = "Yes" + } + rows = append(rows, []string{"Has CA Bundle", hasCaBundle}) } return rows diff --git a/cmd/proxies/create.go b/cmd/proxies/create.go index 6611387..a6b787f 100644 --- a/cmd/proxies/create.go +++ b/cmd/proxies/create.go @@ -3,6 +3,8 @@ package proxies import ( "context" "fmt" + "io" + "os" "strings" "github.com/kernel/cli/pkg/table" @@ -133,6 +135,11 @@ func (p ProxyCmd) Create(ctx context.Context, in ProxyCreateInput) error { return fmt.Errorf("--port is required for custom proxy type") } + caBundle, err := resolveCaBundle(in.CaBundle, in.CaBundleFile) + if err != nil { + return err + } + config := kernel.ProxyNewParamsConfigCustom{ Host: in.Host, Port: int64(in.Port), @@ -143,11 +150,18 @@ func (p ProxyCmd) Create(ctx context.Context, in ProxyCreateInput) error { if in.Password != "" { config.Password = kernel.Opt(in.Password) } + if caBundle != "" { + config.CaBundle = kernel.Opt(caBundle) + } params.Config = kernel.ProxyNewParamsConfigUnion{ OfCustom: &config, } } + if proxyType != kernel.ProxyNewParamsTypeCustom && (in.CaBundle != "" || in.CaBundleFile != "") { + pterm.Warning.Println("--ca-bundle is only supported for custom proxies and will be ignored") + } + // Set protocol (defaults to https if not specified) if in.Protocol != "" { // Validate and convert protocol @@ -195,6 +209,11 @@ func (p ProxyCmd) Create(ctx context.Context, in ProxyCreateInput) error { } rows = append(rows, []string{"Protocol", protocol}) + // The CA bundle is write-only, so confirm the API stored it. + if proxy.Config.HasCaBundle { + rows = append(rows, []string{"Has CA Bundle", "Yes"}) + } + table.PrintTableNoPad(rows, true) return nil } @@ -216,6 +235,8 @@ func runProxiesCreate(cmd *cobra.Command, args []string) error { port, _ := cmd.Flags().GetInt("port") username, _ := cmd.Flags().GetString("username") password, _ := cmd.Flags().GetString("password") + caBundle, _ := cmd.Flags().GetString("ca-bundle") + caBundleFile, _ := cmd.Flags().GetString("ca-bundle-file") bypassHosts, _ := cmd.Flags().GetStringSlice("bypass-host") output, _ := cmd.Flags().GetString("output") @@ -223,24 +244,57 @@ func runProxiesCreate(cmd *cobra.Command, args []string) error { svc := client.Proxies p := ProxyCmd{proxies: &svc} return p.Create(cmd.Context(), ProxyCreateInput{ - Name: name, - Type: proxyType, - Protocol: protocol, - BypassHosts: bypassHosts, - Country: country, - City: city, - State: state, - Zip: zip, - ASN: asn, - OS: os, - Host: host, - Port: port, - Username: username, - Password: password, - Output: output, + Name: name, + Type: proxyType, + Protocol: protocol, + BypassHosts: bypassHosts, + Country: country, + City: city, + State: state, + Zip: zip, + ASN: asn, + OS: os, + Host: host, + Port: port, + Username: username, + Password: password, + CaBundle: caBundle, + CaBundleFile: caBundleFile, + Output: output, }) } +// resolveCaBundle resolves the --ca-bundle / --ca-bundle-file inputs into a +// PEM-encoded CA certificate bundle. The two inputs are mutually exclusive +// (enforced by cobra); a file path of "-" reads stdin. It returns an empty +// string when neither input is set. +func resolveCaBundle(inline, file string) (string, error) { + data := inline + if file != "" { + var b []byte + var err error + if file == "-" { + b, err = io.ReadAll(os.Stdin) + } else { + b, err = os.ReadFile(file) + } + if err != nil { + return "", fmt.Errorf("failed to read CA bundle file: %w", err) + } + data = string(b) + } + + data = strings.TrimSpace(data) + if data == "" { + return "", nil + } + if !strings.Contains(data, "-----BEGIN ") { + return "", fmt.Errorf("invalid CA bundle: expected PEM-encoded certificate data (a '-----BEGIN CERTIFICATE-----' block)") + } + + return data, nil +} + func normalizeBypassHosts(hosts []string) []string { normalized := make([]string, 0, len(hosts)) for _, host := range hosts { diff --git a/cmd/proxies/create_test.go b/cmd/proxies/create_test.go index e985015..ee2fe42 100644 --- a/cmd/proxies/create_test.go +++ b/cmd/proxies/create_test.go @@ -3,11 +3,14 @@ package proxies import ( "context" "errors" + "os" + "path/filepath" "testing" "github.com/kernel/kernel-go-sdk" "github.com/kernel/kernel-go-sdk/option" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestProxyCreate_Datacenter_Success(t *testing.T) { @@ -225,6 +228,116 @@ func TestProxyCreate_Custom_Success(t *testing.T) { assert.Contains(t, output, "Successfully created proxy") } +const testCaBundlePEM = "-----BEGIN CERTIFICATE-----\nMIIBtestbundle\n-----END CERTIFICATE-----" + +func TestProxyCreate_Custom_CaBundleInline(t *testing.T) { + buf := captureOutput(t) + + fake := &FakeProxyService{ + NewFunc: func(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (*kernel.ProxyNewResponse, error) { + customConfig := body.Config.OfCustom + assert.NotNil(t, customConfig) + assert.Equal(t, testCaBundlePEM, customConfig.CaBundle.Value) + + resp := &kernel.ProxyNewResponse{ + ID: "custom-new", + Type: kernel.ProxyNewResponseTypeCustom, + } + resp.Config.HasCaBundle = true + return resp, nil + }, + } + + p := ProxyCmd{proxies: fake} + err := p.Create(context.Background(), ProxyCreateInput{ + Type: "custom", + Host: "proxy.example.com", + Port: 8080, + CaBundle: testCaBundlePEM, + }) + + assert.NoError(t, err) + assert.Contains(t, buf.String(), "Has CA Bundle") +} + +func TestProxyCreate_Custom_CaBundleFromFile(t *testing.T) { + captureOutput(t) + + path := filepath.Join(t.TempDir(), "ca.pem") + // Trailing whitespace is trimmed before the bundle is sent. + require.NoError(t, os.WriteFile(path, []byte(testCaBundlePEM+"\n"), 0o600)) + + var sent string + fake := &FakeProxyService{ + NewFunc: func(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (*kernel.ProxyNewResponse, error) { + sent = body.Config.OfCustom.CaBundle.Value + return &kernel.ProxyNewResponse{ID: "custom-new", Type: kernel.ProxyNewResponseTypeCustom}, nil + }, + } + + p := ProxyCmd{proxies: fake} + err := p.Create(context.Background(), ProxyCreateInput{ + Type: "custom", + Host: "proxy.example.com", + Port: 8080, + CaBundleFile: path, + }) + + assert.NoError(t, err) + assert.Equal(t, testCaBundlePEM, sent) +} + +func TestProxyCreate_Custom_CaBundleFileMissing(t *testing.T) { + captureOutput(t) + + p := ProxyCmd{proxies: &FakeProxyService{}} + err := p.Create(context.Background(), ProxyCreateInput{ + Type: "custom", + Host: "proxy.example.com", + Port: 8080, + CaBundleFile: filepath.Join(t.TempDir(), "does-not-exist.pem"), + }) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to read CA bundle file") +} + +func TestProxyCreate_Custom_CaBundleNotPEM(t *testing.T) { + captureOutput(t) + + p := ProxyCmd{proxies: &FakeProxyService{}} + err := p.Create(context.Background(), ProxyCreateInput{ + Type: "custom", + Host: "proxy.example.com", + Port: 8080, + CaBundle: "not a certificate", + }) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid CA bundle: expected PEM-encoded certificate data") +} + +func TestProxyCreate_CaBundleIgnoredForNonCustom(t *testing.T) { + buf := captureOutput(t) + + fake := &FakeProxyService{ + NewFunc: func(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (*kernel.ProxyNewResponse, error) { + assert.Nil(t, body.Config.OfCustom) + return &kernel.ProxyNewResponse{ID: "dc-new", Type: kernel.ProxyNewResponseTypeDatacenter}, nil + }, + } + + p := ProxyCmd{proxies: fake} + err := p.Create(context.Background(), ProxyCreateInput{ + Type: "datacenter", + Country: "US", + CaBundle: testCaBundlePEM, + }) + + assert.NoError(t, err) + assert.Contains(t, buf.String(), "--ca-bundle is only supported for custom proxies") +} + func TestProxyCreate_Custom_MissingHost(t *testing.T) { fake := &FakeProxyService{} diff --git a/cmd/proxies/get.go b/cmd/proxies/get.go index c0fc5c2..e5a7aa8 100644 --- a/cmd/proxies/get.go +++ b/cmd/proxies/get.go @@ -126,6 +126,11 @@ func getProxyConfigRows(proxy *kernel.ProxyGetResponse) [][]string { hasPassword = "Yes" } rows = append(rows, []string{"Has Password", hasPassword}) + hasCaBundle := "No" + if config.HasCaBundle { + hasCaBundle = "Yes" + } + rows = append(rows, []string{"Has CA Bundle", hasCaBundle}) } return rows diff --git a/cmd/proxies/list.go b/cmd/proxies/list.go index a7ff4bc..83a8f74 100644 --- a/cmd/proxies/list.go +++ b/cmd/proxies/list.go @@ -141,7 +141,11 @@ func formatProxyConfig(proxy *kernel.ProxyListResponse) string { if config.Username != "" { auth = fmt.Sprintf(", Auth: %s", config.Username) } - return fmt.Sprintf("%s:%d%s", config.Host, config.Port, auth) + caBundle := "" + if config.HasCaBundle { + caBundle = ", CA bundle" + } + return fmt.Sprintf("%s:%d%s%s", config.Host, config.Port, auth, caBundle) } } return "-" diff --git a/cmd/proxies/proxies.go b/cmd/proxies/proxies.go index 8820939..33a9fa7 100644 --- a/cmd/proxies/proxies.go +++ b/cmd/proxies/proxies.go @@ -51,6 +51,9 @@ Examples: # Create a residential proxy with location kernel proxies create --type residential --country US --city sanfrancisco --state CA + # Create a custom TLS-terminating (MITM) proxy with its CA bundle + kernel proxies create --type custom --host proxy.example.com --port 8080 --ca-bundle-file ./mitm-ca.pem + # Create a proxy with bypass hosts kernel proxies create --type datacenter --country US --bypass-host localhost,internal.service.local`, RunE: runProxiesCreate, @@ -116,6 +119,9 @@ func init() { proxiesCreateCmd.Flags().Int("port", 0, "Proxy port") proxiesCreateCmd.Flags().String("username", "", "Username for proxy authentication") proxiesCreateCmd.Flags().String("password", "", "Password for proxy authentication") + proxiesCreateCmd.Flags().String("ca-bundle", "", "PEM-encoded CA certificate bundle for a TLS-terminating (MITM) custom proxy") + proxiesCreateCmd.Flags().String("ca-bundle-file", "", "Read the PEM-encoded CA certificate bundle from a file (use '-' for stdin)") + proxiesCreateCmd.MarkFlagsMutuallyExclusive("ca-bundle", "ca-bundle-file") proxiesCreateCmd.Flags().StringSlice("bypass-host", nil, "Hostname(s) to bypass proxy and connect directly (repeat or comma-separated)") // Update flags diff --git a/cmd/proxies/types.go b/cmd/proxies/types.go index ca44ec2..f52a403 100644 --- a/cmd/proxies/types.go +++ b/cmd/proxies/types.go @@ -56,7 +56,10 @@ type ProxyCreateInput struct { Port int Username string Password string - Output string + // PEM-encoded CA bundle for MITM proxies, provided inline or read from a file. + CaBundle string + CaBundleFile string + Output string } type ProxyUpdateInput struct { diff --git a/go.mod b/go.mod index 3d90ae6..54ec0a7 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/joho/godotenv v1.5.1 - github.com/kernel/kernel-go-sdk v0.84.0 + github.com/kernel/kernel-go-sdk v0.85.0 github.com/klauspost/compress v1.18.5 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 diff --git a/go.sum b/go.sum index 9341ef3..a872a5b 100644 --- a/go.sum +++ b/go.sum @@ -64,8 +64,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kernel/kernel-go-sdk v0.84.0 h1:EYJC6TMFDiosqsbWbCLlVkVr/B8/hTYfyVAUinb7Hsk= -github.com/kernel/kernel-go-sdk v0.84.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.85.0 h1:rACZOx5dcjO4rasFmMoh2GS14w4yRBbYXxb92eGvD2E= +github.com/kernel/kernel-go-sdk v0.85.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= From 7f6bf412808e7cef8a2e11574303f518456d0a6e Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 13:48:19 +0000 Subject: [PATCH 3/4] CLI: Verify SDK v0.85.0 coverage, add managed-auth and replay flags SDK e6dff4a is the v0.85.0 tag the CLI already pins, so go.mod is unchanged and the api.md diff against the previous version is empty. A full enumeration of all 128 SDK methods and 286 request-param fields was performed anyway, which surfaced pre-existing coverage gaps that are closed here. Fix broken merge in cmd/proxies: The merge of main into this branch unioned two independent implementations of the proxy CA bundle feature, leaving the tree uncompilable (undefined io, in.CaBundle on a struct without that field). Resolved to main's shipped --ca-bundle semantics and dropped the duplicate inline/stdin variant, which would otherwise have silently redefined --ca-bundle for existing users. New flags: - kernel auth connections create --record-session / --no-health-checks / --no-auto-reauth (ManagedAuthCreateRequestParam.{RecordSession,HealthChecks, AutoReauth}) - kernel auth connections update --record-session / --no-record-session, --health-checks / --no-health-checks, --auto-reauth / --no-auto-reauth (ManagedAuthUpdateRequestParam, tri-state: omitting both leaves the connection's current value untouched) - kernel auth connections login --record-session / --no-record-session (AuthConnectionLoginParams.RecordSession, per-login override) - kernel browsers replays start --record-audio (BrowserReplayStartParams.RecordAudio) - kernel credentials update --remove-value-key (UpdateCredentialRequestParam.RemoveValueKeys) BrowserCurlParams.{TimeoutMs,ResponseEncoding} are intentionally left unexposed: kernel browsers curl does not call the SDK's Curl method, it proxies through a raw HTTP client for binary-safe streaming. --max-time already covers the timeout, and response encoding is meaningless when raw bytes go straight to stdout. Tested against the live API: - auth connections create --record-session --no-health-checks --no-auto-reauth -> response echoed record_session=true, health_checks=false, auto_reauth=false - auth connections update, both directions, plus an unrelated-field-only update confirming omitted toggles are left untouched - mutually-exclusive pairs rejected (--health-checks with --no-health-checks) - credentials update --remove-value-key removed the key; combined with --value on the same key, the new value is kept (removal applied first) - browsers replays start --record-audio -> started, stopped, listed - request bodies captured against a local listener confirming tri-state on the wire: {"record_audio":true} / {} and {"record_session":true} / {"record_session":false} / {} - all test resources deleted; go build, go vet, gofmt and go test ./... pass Triggered by: kernel/kernel-go-sdk@e6dff4a41334b0008f7d0469bf83477905cdc589 Co-Authored-By: Claude Opus 5 --- cmd/auth_connections.go | 101 ++++++++++++++++++++++++++++++++++++---- cmd/browsers.go | 8 +++- cmd/credentials.go | 31 +++++++----- cmd/proxies/create.go | 38 +-------------- cmd/proxies/proxies.go | 3 -- 5 files changed, 118 insertions(+), 63 deletions(-) diff --git a/cmd/auth_connections.go b/cmd/auth_connections.go index 87f47d4..bd94317 100644 --- a/cmd/auth_connections.go +++ b/cmd/auth_connections.go @@ -51,6 +51,9 @@ type AuthConnectionCreateInput struct { SaveCredentials bool NoSaveCredentials bool HealthCheckInterval int + NoHealthChecks bool + NoAutoReauth bool + RecordSession bool Telemetry string Output string } @@ -80,6 +83,9 @@ type AuthConnectionUpdateInput struct { SaveCredentials BoolFlag HealthCheckInterval int HealthCheckIntervalSet bool + HealthChecks BoolFlag + AutoReauth BoolFlag + RecordSession BoolFlag Telemetry string Output string } @@ -98,11 +104,12 @@ type AuthConnectionDeleteInput struct { } type AuthConnectionLoginInput struct { - ID string - ProxyID string - ProxyName string - Telemetry string - Output string + ID string + ProxyID string + ProxyName string + RecordSession BoolFlag + Telemetry string + Output string } type AuthConnectionSubmitInput struct { @@ -201,6 +208,18 @@ func (c AuthConnectionCmd) Create(ctx context.Context, in AuthConnectionCreateIn params.ManagedAuthCreateRequest.SaveCredentials = kernel.Opt(false) } + if in.NoHealthChecks { + params.ManagedAuthCreateRequest.HealthChecks = kernel.Opt(false) + } + + if in.NoAutoReauth { + params.ManagedAuthCreateRequest.AutoReauth = kernel.Opt(false) + } + + if in.RecordSession { + params.ManagedAuthCreateRequest.RecordSession = kernel.Opt(true) + } + if in.Telemetry != "" { t, err := buildAuthConnectionCreateTelemetryParam(in.Telemetry) if err != nil { @@ -276,6 +295,18 @@ func (c AuthConnectionCmd) Update(ctx context.Context, in AuthConnectionUpdateIn params.ManagedAuthUpdateRequest.SaveCredentials = kernel.Opt(in.SaveCredentials.Value) hasChanges = true } + if in.HealthChecks.Set { + params.ManagedAuthUpdateRequest.HealthChecks = kernel.Opt(in.HealthChecks.Value) + hasChanges = true + } + if in.AutoReauth.Set { + params.ManagedAuthUpdateRequest.AutoReauth = kernel.Opt(in.AutoReauth.Value) + hasChanges = true + } + if in.RecordSession.Set { + params.ManagedAuthUpdateRequest.RecordSession = kernel.Opt(in.RecordSession.Value) + hasChanges = true + } if in.AllowedDomainsSet { params.ManagedAuthUpdateRequest.AllowedDomains = in.AllowedDomains hasChanges = true @@ -620,6 +651,10 @@ func (c AuthConnectionCmd) Login(ctx context.Context, in AuthConnectionLoginInpu } } + if in.RecordSession.Set { + params.RecordSession = kernel.Opt(in.RecordSession.Value) + } + if in.Telemetry != "" { t, err := buildAuthConnectionLoginTelemetryParam(in.Telemetry) if err != nil { @@ -1058,6 +1093,9 @@ func init() { authConnectionsCreateCmd.Flags().String("proxy-name", "", "Proxy name to use") authConnectionsCreateCmd.Flags().Bool("no-save-credentials", false, "Disable saving credentials after successful login") authConnectionsCreateCmd.Flags().Int("health-check-interval", 0, "Interval in seconds between health checks (300-86400)") + authConnectionsCreateCmd.Flags().Bool("no-health-checks", false, "Disable periodic health checks (enabled by default)") + authConnectionsCreateCmd.Flags().Bool("no-auto-reauth", false, "Mark expired sessions as NEEDS_AUTH instead of attempting automatic re-authentication (auto re-auth is enabled by default)") + authConnectionsCreateCmd.Flags().Bool("record-session", false, "Record browser sessions for this connection by default (useful for debugging)") authConnectionsCreateCmd.Flags().String("telemetry", "", "Configure telemetry for this connection's browser sessions (opt-in): --telemetry=all (default set), --telemetry=off (disable), or --telemetry=console,network (capture exactly those categories)") _ = authConnectionsCreateCmd.MarkFlagRequired("domain") _ = authConnectionsCreateCmd.MarkFlagRequired("profile-name") @@ -1079,9 +1117,18 @@ func init() { authConnectionsUpdateCmd.Flags().Bool("save-credentials", false, "Enable saving credentials after successful login") authConnectionsUpdateCmd.Flags().Bool("no-save-credentials", false, "Disable saving credentials after successful login") authConnectionsUpdateCmd.Flags().Int("health-check-interval", 0, "Interval in seconds between health checks") + authConnectionsUpdateCmd.Flags().Bool("health-checks", false, "Enable periodic health checks") + authConnectionsUpdateCmd.Flags().Bool("no-health-checks", false, "Disable periodic health checks") + authConnectionsUpdateCmd.Flags().Bool("auto-reauth", false, "Permit automatic re-authentication when a health check detects an expired session") + authConnectionsUpdateCmd.Flags().Bool("no-auto-reauth", false, "Mark expired sessions as NEEDS_AUTH instead of attempting automatic re-authentication") + authConnectionsUpdateCmd.Flags().Bool("record-session", false, "Record browser sessions for this connection by default") + authConnectionsUpdateCmd.Flags().Bool("no-record-session", false, "Stop recording browser sessions for this connection by default") authConnectionsUpdateCmd.Flags().String("telemetry", "", "Update telemetry for future browser sessions: --telemetry=all (reset to default set), --telemetry=off (disable), or --telemetry=console,network (merge those categories into the current selection)") authConnectionsUpdateCmd.MarkFlagsMutuallyExclusive("credential-name", "credential-provider") authConnectionsUpdateCmd.MarkFlagsMutuallyExclusive("save-credentials", "no-save-credentials") + authConnectionsUpdateCmd.MarkFlagsMutuallyExclusive("health-checks", "no-health-checks") + authConnectionsUpdateCmd.MarkFlagsMutuallyExclusive("auto-reauth", "no-auto-reauth") + authConnectionsUpdateCmd.MarkFlagsMutuallyExclusive("record-session", "no-record-session") // List flags addJSONOutputFlag(authConnectionsListCmd) @@ -1097,6 +1144,9 @@ func init() { addJSONOutputFlag(authConnectionsLoginCmd) authConnectionsLoginCmd.Flags().String("proxy-id", "", "Proxy ID to use for this login") authConnectionsLoginCmd.Flags().String("proxy-name", "", "Proxy name to use for this login") + authConnectionsLoginCmd.Flags().Bool("record-session", false, "Record this login's browser session, overriding the connection's default") + authConnectionsLoginCmd.Flags().Bool("no-record-session", false, "Do not record this login's browser session, overriding the connection's default") + authConnectionsLoginCmd.MarkFlagsMutuallyExclusive("record-session", "no-record-session") authConnectionsLoginCmd.Flags().String("telemetry", "", "Telemetry override for this login only, merged onto the connection's config: --telemetry=all, --telemetry=off, or --telemetry=console,network") // Submit flags @@ -1147,6 +1197,9 @@ func runAuthConnectionsCreate(cmd *cobra.Command, args []string) error { proxyName, _ := cmd.Flags().GetString("proxy-name") noSaveCredentials, _ := cmd.Flags().GetBool("no-save-credentials") healthCheckInterval, _ := cmd.Flags().GetInt("health-check-interval") + noHealthChecks, _ := cmd.Flags().GetBool("no-health-checks") + noAutoReauth, _ := cmd.Flags().GetBool("no-auto-reauth") + recordSession, _ := cmd.Flags().GetBool("record-session") telemetry, _ := cmd.Flags().GetString("telemetry") svc := client.Auth.Connections @@ -1164,6 +1217,9 @@ func runAuthConnectionsCreate(cmd *cobra.Command, args []string) error { ProxyName: proxyName, NoSaveCredentials: noSaveCredentials, HealthCheckInterval: healthCheckInterval, + NoHealthChecks: noHealthChecks, + NoAutoReauth: noAutoReauth, + RecordSession: recordSession, Telemetry: telemetry, Output: output, }) @@ -1198,6 +1254,7 @@ func runAuthConnectionsUpdate(cmd *cobra.Command, args []string) error { telemetry, _ := cmd.Flags().GetString("telemetry") saveCredentialsFlag := BoolFlag{} + if cmd.Flags().Changed("save-credentials") { saveCredentialsFlag = BoolFlag{Set: true, Value: saveCredentials} } @@ -1205,6 +1262,20 @@ func runAuthConnectionsUpdate(cmd *cobra.Command, args []string) error { saveCredentialsFlag = BoolFlag{Set: true, Value: !noSaveCredentials} } + // Each of these is a tri-state override: --x sets true, --no-x sets false, and + // omitting both leaves the connection's current value untouched. + togglePair := func(onFlag, offFlag string) BoolFlag { + if cmd.Flags().Changed(onFlag) { + v, _ := cmd.Flags().GetBool(onFlag) + return BoolFlag{Set: true, Value: v} + } + if cmd.Flags().Changed(offFlag) { + v, _ := cmd.Flags().GetBool(offFlag) + return BoolFlag{Set: true, Value: !v} + } + return BoolFlag{} + } + svc := client.Auth.Connections c := AuthConnectionCmd{svc: &svc} return c.Update(cmd.Context(), AuthConnectionUpdateInput{ @@ -1227,6 +1298,9 @@ func runAuthConnectionsUpdate(cmd *cobra.Command, args []string) error { SaveCredentials: saveCredentialsFlag, HealthCheckInterval: healthCheckInterval, HealthCheckIntervalSet: cmd.Flags().Changed("health-check-interval"), + HealthChecks: togglePair("health-checks", "no-health-checks"), + AutoReauth: togglePair("auto-reauth", "no-auto-reauth"), + RecordSession: togglePair("record-session", "no-record-session"), Telemetry: telemetry, Output: output, }) @@ -1270,14 +1344,21 @@ func runAuthConnectionsLogin(cmd *cobra.Command, args []string) error { proxyName, _ := cmd.Flags().GetString("proxy-name") telemetry, _ := cmd.Flags().GetString("telemetry") + var recordSessionFlag BoolFlag + if cmd.Flags().Changed("record-session") || cmd.Flags().Changed("no-record-session") { + noRecordSession, _ := cmd.Flags().GetBool("no-record-session") + recordSessionFlag = BoolFlag{Set: true, Value: !noRecordSession} + } + svc := client.Auth.Connections c := AuthConnectionCmd{svc: &svc} return c.Login(cmd.Context(), AuthConnectionLoginInput{ - ID: args[0], - ProxyID: proxyID, - ProxyName: proxyName, - Telemetry: telemetry, - Output: output, + ID: args[0], + ProxyID: proxyID, + ProxyName: proxyName, + RecordSession: recordSessionFlag, + Telemetry: telemetry, + Output: output, }) } diff --git a/cmd/browsers.go b/cmd/browsers.go index 88f520c..0561184 100644 --- a/cmd/browsers.go +++ b/cmd/browsers.go @@ -1335,6 +1335,7 @@ type BrowsersReplaysStartInput struct { Identifier string Framerate int MaxDurationSeconds int + RecordAudio bool Output string } @@ -1395,6 +1396,9 @@ func (b BrowsersCmd) ReplaysStart(ctx context.Context, in BrowsersReplaysStartIn if in.MaxDurationSeconds > 0 { body.MaxDurationInSeconds = kernel.Opt(int64(in.MaxDurationSeconds)) } + if in.RecordAudio { + body.RecordAudio = kernel.Opt(true) + } res, err := b.replays.Start(ctx, br.SessionID, body) if err != nil { return util.CleanedUpSdkError{Err: err} @@ -2536,6 +2540,7 @@ func init() { replaysStart := &cobra.Command{Use: "start ", Short: "Start a replay recording", Args: cobra.ExactArgs(1), RunE: runBrowsersReplaysStart} replaysStart.Flags().Int("framerate", 0, "Recording framerate (fps)") replaysStart.Flags().Int("max-duration", 0, "Maximum duration in seconds") + replaysStart.Flags().Bool("record-audio", false, "Record audio in addition to video (default is video-only)") addJSONOutputFlag(replaysStart) replaysStop := &cobra.Command{Use: "stop ", Short: "Stop a replay recording", Args: cobra.ExactArgs(2), RunE: runBrowsersReplaysStop} replaysDownload := &cobra.Command{Use: "download ", Short: "Download a replay video", Args: cobra.ExactArgs(2), RunE: runBrowsersReplaysDownload} @@ -3141,9 +3146,10 @@ func runBrowsersReplaysStart(cmd *cobra.Command, args []string) error { svc := client.Browsers fr, _ := cmd.Flags().GetInt("framerate") md, _ := cmd.Flags().GetInt("max-duration") + recordAudio, _ := cmd.Flags().GetBool("record-audio") output, _ := cmd.Flags().GetString("output") b := BrowsersCmd{browsers: &svc, replays: &svc.Replays} - return b.ReplaysStart(cmd.Context(), BrowsersReplaysStartInput{Identifier: args[0], Framerate: fr, MaxDurationSeconds: md, Output: output}) + return b.ReplaysStart(cmd.Context(), BrowsersReplaysStartInput{Identifier: args[0], Framerate: fr, MaxDurationSeconds: md, RecordAudio: recordAudio, Output: output}) } func runBrowsersReplaysStop(cmd *cobra.Command, args []string) error { diff --git a/cmd/credentials.go b/cmd/credentials.go index 8a58370..56fcbdc 100644 --- a/cmd/credentials.go +++ b/cmd/credentials.go @@ -52,12 +52,13 @@ type CredentialsCreateInput struct { } type CredentialsUpdateInput struct { - Identifier string - Name string - SSOProvider string - TotpSecret string - Values map[string]string - Output string + Identifier string + Name string + SSOProvider string + TotpSecret string + Values map[string]string + RemoveValueKeys []string + Output string } type CredentialsDeleteInput struct { @@ -263,6 +264,9 @@ func (c CredentialsCmd) Update(ctx context.Context, in CredentialsUpdateInput) e if len(in.Values) > 0 { params.UpdateCredentialRequest.Values = in.Values } + if len(in.RemoveValueKeys) > 0 { + params.UpdateCredentialRequest.RemoveValueKeys = in.RemoveValueKeys + } if in.Output != "json" { pterm.Info.Printf("Updating credential '%s'...\n", in.Identifier) @@ -428,6 +432,7 @@ func init() { credentialsUpdateCmd.Flags().String("sso-provider", "", "SSO provider (set to empty string to remove)") credentialsUpdateCmd.Flags().String("totp-secret", "", "Base32-encoded TOTP secret (set to empty string to remove)") credentialsUpdateCmd.Flags().StringArray("value", []string{}, "Field name=value pair to update (repeatable)") + credentialsUpdateCmd.Flags().StringArray("remove-value-key", []string{}, "Field name to remove from the credential's stored values (repeatable). Removals are applied before --value is merged, so a key given to both keeps its new value") // Delete flags credentialsDeleteCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt") @@ -503,6 +508,7 @@ func runCredentialsUpdate(cmd *cobra.Command, args []string) error { ssoProvider, _ := cmd.Flags().GetString("sso-provider") totpSecret, _ := cmd.Flags().GetString("totp-secret") valuePairs, _ := cmd.Flags().GetStringArray("value") + removeValueKeys, _ := cmd.Flags().GetStringArray("remove-value-key") // Parse value pairs into map values := make(map[string]string) @@ -517,12 +523,13 @@ func runCredentialsUpdate(cmd *cobra.Command, args []string) error { svc := client.Credentials c := CredentialsCmd{credentials: &svc} return c.Update(cmd.Context(), CredentialsUpdateInput{ - Identifier: args[0], - Name: name, - SSOProvider: ssoProvider, - TotpSecret: totpSecret, - Values: values, - Output: output, + Identifier: args[0], + Name: name, + SSOProvider: ssoProvider, + TotpSecret: totpSecret, + Values: values, + RemoveValueKeys: removeValueKeys, + Output: output, }) } diff --git a/cmd/proxies/create.go b/cmd/proxies/create.go index e451aa2..68e4675 100644 --- a/cmd/proxies/create.go +++ b/cmd/proxies/create.go @@ -134,11 +134,6 @@ func (p ProxyCmd) Create(ctx context.Context, in ProxyCreateInput) error { return fmt.Errorf("--port is required for custom proxy type") } - caBundle, err := resolveCaBundle(in.CaBundle, in.CaBundleFile) - if err != nil { - return err - } - config := kernel.ProxyNewParamsConfigCustom{ Host: in.Host, Port: int64(in.Port), @@ -164,7 +159,7 @@ func (p ProxyCmd) Create(ctx context.Context, in ProxyCreateInput) error { } } - if proxyType != kernel.ProxyNewParamsTypeCustom && (in.CaBundle != "" || in.CaBundleFile != "") { + if proxyType != kernel.ProxyNewParamsTypeCustom && in.CaBundleFile != "" { pterm.Warning.Println("--ca-bundle is only supported for custom proxies and will be ignored") } @@ -268,37 +263,6 @@ func runProxiesCreate(cmd *cobra.Command, args []string) error { }) } -// resolveCaBundle resolves the --ca-bundle / --ca-bundle-file inputs into a -// PEM-encoded CA certificate bundle. The two inputs are mutually exclusive -// (enforced by cobra); a file path of "-" reads stdin. It returns an empty -// string when neither input is set. -func resolveCaBundle(inline, file string) (string, error) { - data := inline - if file != "" { - var b []byte - var err error - if file == "-" { - b, err = io.ReadAll(os.Stdin) - } else { - b, err = os.ReadFile(file) - } - if err != nil { - return "", fmt.Errorf("failed to read CA bundle file: %w", err) - } - data = string(b) - } - - data = strings.TrimSpace(data) - if data == "" { - return "", nil - } - if !strings.Contains(data, "-----BEGIN ") { - return "", fmt.Errorf("invalid CA bundle: expected PEM-encoded certificate data (a '-----BEGIN CERTIFICATE-----' block)") - } - - return data, nil -} - func normalizeBypassHosts(hosts []string) []string { normalized := make([]string, 0, len(hosts)) for _, host := range hosts { diff --git a/cmd/proxies/proxies.go b/cmd/proxies/proxies.go index b2e4e43..7642001 100644 --- a/cmd/proxies/proxies.go +++ b/cmd/proxies/proxies.go @@ -54,9 +54,6 @@ Examples: # Create a residential proxy with location kernel proxies create --type residential --country US --city sanfrancisco --state CA --name "SF Residential" - # Create a custom TLS-terminating (MITM) proxy with its CA bundle - kernel proxies create --type custom --host proxy.example.com --port 8080 --ca-bundle-file ./mitm-ca.pem - # Create a proxy with bypass hosts kernel proxies create --type datacenter --country US --bypass-host localhost,internal.service.local --name "Internal Services"`, RunE: runProxiesCreate, From 174f8572af58a421187181d50c2b9d54780f0a75 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Fri, 31 Jul 2026 11:01:43 -0400 Subject: [PATCH 4/4] fix(auth): simplify record-session flag state --- cmd/auth_connections.go | 27 ++++--------- cmd/auth_connections_test.go | 74 ++++++++++++++++++++++++++++++++++++ cmd/flag_values.go | 9 +++++ cmd/flag_values_test.go | 33 ++++++++++++++++ 4 files changed, 124 insertions(+), 19 deletions(-) create mode 100644 cmd/flag_values_test.go diff --git a/cmd/auth_connections.go b/cmd/auth_connections.go index bd94317..1f8b5f1 100644 --- a/cmd/auth_connections.go +++ b/cmd/auth_connections.go @@ -53,7 +53,7 @@ type AuthConnectionCreateInput struct { HealthCheckInterval int NoHealthChecks bool NoAutoReauth bool - RecordSession bool + RecordSession BoolFlag Telemetry string Output string } @@ -216,8 +216,8 @@ func (c AuthConnectionCmd) Create(ctx context.Context, in AuthConnectionCreateIn params.ManagedAuthCreateRequest.AutoReauth = kernel.Opt(false) } - if in.RecordSession { - params.ManagedAuthCreateRequest.RecordSession = kernel.Opt(true) + if in.RecordSession.Set { + params.ManagedAuthCreateRequest.RecordSession = kernel.Opt(in.RecordSession.Value) } if in.Telemetry != "" { @@ -1121,14 +1121,12 @@ func init() { authConnectionsUpdateCmd.Flags().Bool("no-health-checks", false, "Disable periodic health checks") authConnectionsUpdateCmd.Flags().Bool("auto-reauth", false, "Permit automatic re-authentication when a health check detects an expired session") authConnectionsUpdateCmd.Flags().Bool("no-auto-reauth", false, "Mark expired sessions as NEEDS_AUTH instead of attempting automatic re-authentication") - authConnectionsUpdateCmd.Flags().Bool("record-session", false, "Record browser sessions for this connection by default") - authConnectionsUpdateCmd.Flags().Bool("no-record-session", false, "Stop recording browser sessions for this connection by default") + authConnectionsUpdateCmd.Flags().Bool("record-session", false, "Set whether browser sessions are recorded by default; use --record-session=false to disable") authConnectionsUpdateCmd.Flags().String("telemetry", "", "Update telemetry for future browser sessions: --telemetry=all (reset to default set), --telemetry=off (disable), or --telemetry=console,network (merge those categories into the current selection)") authConnectionsUpdateCmd.MarkFlagsMutuallyExclusive("credential-name", "credential-provider") authConnectionsUpdateCmd.MarkFlagsMutuallyExclusive("save-credentials", "no-save-credentials") authConnectionsUpdateCmd.MarkFlagsMutuallyExclusive("health-checks", "no-health-checks") authConnectionsUpdateCmd.MarkFlagsMutuallyExclusive("auto-reauth", "no-auto-reauth") - authConnectionsUpdateCmd.MarkFlagsMutuallyExclusive("record-session", "no-record-session") // List flags addJSONOutputFlag(authConnectionsListCmd) @@ -1144,9 +1142,7 @@ func init() { addJSONOutputFlag(authConnectionsLoginCmd) authConnectionsLoginCmd.Flags().String("proxy-id", "", "Proxy ID to use for this login") authConnectionsLoginCmd.Flags().String("proxy-name", "", "Proxy name to use for this login") - authConnectionsLoginCmd.Flags().Bool("record-session", false, "Record this login's browser session, overriding the connection's default") - authConnectionsLoginCmd.Flags().Bool("no-record-session", false, "Do not record this login's browser session, overriding the connection's default") - authConnectionsLoginCmd.MarkFlagsMutuallyExclusive("record-session", "no-record-session") + authConnectionsLoginCmd.Flags().Bool("record-session", false, "Override whether this login's browser session is recorded; use --record-session=false to disable") authConnectionsLoginCmd.Flags().String("telemetry", "", "Telemetry override for this login only, merged onto the connection's config: --telemetry=all, --telemetry=off, or --telemetry=console,network") // Submit flags @@ -1199,7 +1195,6 @@ func runAuthConnectionsCreate(cmd *cobra.Command, args []string) error { healthCheckInterval, _ := cmd.Flags().GetInt("health-check-interval") noHealthChecks, _ := cmd.Flags().GetBool("no-health-checks") noAutoReauth, _ := cmd.Flags().GetBool("no-auto-reauth") - recordSession, _ := cmd.Flags().GetBool("record-session") telemetry, _ := cmd.Flags().GetString("telemetry") svc := client.Auth.Connections @@ -1219,7 +1214,7 @@ func runAuthConnectionsCreate(cmd *cobra.Command, args []string) error { HealthCheckInterval: healthCheckInterval, NoHealthChecks: noHealthChecks, NoAutoReauth: noAutoReauth, - RecordSession: recordSession, + RecordSession: readBoolFlag(cmd.Flags(), "record-session"), Telemetry: telemetry, Output: output, }) @@ -1300,7 +1295,7 @@ func runAuthConnectionsUpdate(cmd *cobra.Command, args []string) error { HealthCheckIntervalSet: cmd.Flags().Changed("health-check-interval"), HealthChecks: togglePair("health-checks", "no-health-checks"), AutoReauth: togglePair("auto-reauth", "no-auto-reauth"), - RecordSession: togglePair("record-session", "no-record-session"), + RecordSession: readBoolFlag(cmd.Flags(), "record-session"), Telemetry: telemetry, Output: output, }) @@ -1344,19 +1339,13 @@ func runAuthConnectionsLogin(cmd *cobra.Command, args []string) error { proxyName, _ := cmd.Flags().GetString("proxy-name") telemetry, _ := cmd.Flags().GetString("telemetry") - var recordSessionFlag BoolFlag - if cmd.Flags().Changed("record-session") || cmd.Flags().Changed("no-record-session") { - noRecordSession, _ := cmd.Flags().GetBool("no-record-session") - recordSessionFlag = BoolFlag{Set: true, Value: !noRecordSession} - } - svc := client.Auth.Connections c := AuthConnectionCmd{svc: &svc} return c.Login(cmd.Context(), AuthConnectionLoginInput{ ID: args[0], ProxyID: proxyID, ProxyName: proxyName, - RecordSession: recordSessionFlag, + RecordSession: readBoolFlag(cmd.Flags(), "record-session"), Telemetry: telemetry, Output: output, }) diff --git a/cmd/auth_connections_test.go b/cmd/auth_connections_test.go index 4b9bca9..ada9cb1 100644 --- a/cmd/auth_connections_test.go +++ b/cmd/auth_connections_test.go @@ -329,6 +329,42 @@ func TestAuthConnectionsCreate_CredentialName_UnaffectedByAutoDefault(t *testing assert.False(t, cred.Path.Valid()) } +func TestAuthConnectionsCreate_RecordSession(t *testing.T) { + tests := []struct { + name string + flag BoolFlag + }{ + {name: "omitted"}, + {name: "enabled", flag: BoolFlag{Set: true, Value: true}}, + {name: "disabled", flag: BoolFlag{Set: true, Value: false}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var captured kernel.AuthConnectionNewParams + fake := &FakeAuthConnectionService{ + NewFunc: func(ctx context.Context, body kernel.AuthConnectionNewParams, opts ...option.RequestOption) (*kernel.ManagedAuth, error) { + captured = body + return &kernel.ManagedAuth{ID: "conn-new"}, nil + }, + } + + c := AuthConnectionCmd{svc: fake} + require.NoError(t, c.Create(context.Background(), AuthConnectionCreateInput{ + Domain: "example.com", + ProfileName: "profile-1", + RecordSession: tt.flag, + Output: "json", + })) + + assert.Equal(t, tt.flag.Set, captured.ManagedAuthCreateRequest.RecordSession.Valid()) + if tt.flag.Set { + assert.Equal(t, tt.flag.Value, captured.ManagedAuthCreateRequest.RecordSession.Value) + } + }) + } +} + func TestAuthConnectionsUpdate_MapsParams(t *testing.T) { var captured kernel.AuthConnectionUpdateParams fake := &FakeAuthConnectionService{ @@ -358,6 +394,7 @@ func TestAuthConnectionsUpdate_MapsParams(t *testing.T) { ProxyID: "proxy-123", ProxyIDSet: true, SaveCredentials: BoolFlag{Set: true, Value: false}, + RecordSession: BoolFlag{Set: true, Value: false}, HealthCheckInterval: 900, HealthCheckIntervalSet: true, }) @@ -375,10 +412,47 @@ func TestAuthConnectionsUpdate_MapsParams(t *testing.T) { assert.Equal(t, "proxy-123", captured.ManagedAuthUpdateRequest.Proxy.ID.Value) require.True(t, captured.ManagedAuthUpdateRequest.SaveCredentials.Valid()) assert.False(t, captured.ManagedAuthUpdateRequest.SaveCredentials.Value) + require.True(t, captured.ManagedAuthUpdateRequest.RecordSession.Valid()) + assert.False(t, captured.ManagedAuthUpdateRequest.RecordSession.Value) require.True(t, captured.ManagedAuthUpdateRequest.HealthCheckInterval.Valid()) assert.Equal(t, int64(900), captured.ManagedAuthUpdateRequest.HealthCheckInterval.Value) } +func TestAuthConnectionsLogin_RecordSession(t *testing.T) { + tests := []struct { + name string + flag BoolFlag + }{ + {name: "inherits connection default"}, + {name: "enabled", flag: BoolFlag{Set: true, Value: true}}, + {name: "disabled", flag: BoolFlag{Set: true, Value: false}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var captured kernel.AuthConnectionLoginParams + fake := &FakeAuthConnectionService{ + LoginFunc: func(ctx context.Context, id string, body kernel.AuthConnectionLoginParams, opts ...option.RequestOption) (*kernel.LoginResponse, error) { + captured = body + return &kernel.LoginResponse{}, nil + }, + } + + c := AuthConnectionCmd{svc: fake} + require.NoError(t, c.Login(context.Background(), AuthConnectionLoginInput{ + ID: "conn-1", + RecordSession: tt.flag, + Output: "json", + })) + + assert.Equal(t, tt.flag.Set, captured.RecordSession.Valid()) + if tt.flag.Set { + assert.Equal(t, tt.flag.Value, captured.RecordSession.Value) + } + }) + } +} + func newFakeWithMfaOptions(options []kernel.ManagedAuthMfaOption) *FakeAuthConnectionService { return &FakeAuthConnectionService{ GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ManagedAuth, error) { diff --git a/cmd/flag_values.go b/cmd/flag_values.go index b92b315..27d3833 100644 --- a/cmd/flag_values.go +++ b/cmd/flag_values.go @@ -1,11 +1,20 @@ package cmd +import "github.com/spf13/pflag" + // BoolFlag captures whether a boolean flag was set explicitly and its value. type BoolFlag struct { Set bool Value bool } +// readBoolFlag preserves the distinction between an omitted flag and an +// explicitly false value. +func readBoolFlag(flags *pflag.FlagSet, name string) BoolFlag { + value, _ := flags.GetBool(name) + return BoolFlag{Set: flags.Changed(name), Value: value} +} + // Int64Flag captures whether an int64 flag was set explicitly and its value. type Int64Flag struct { Set bool diff --git a/cmd/flag_values_test.go b/cmd/flag_values_test.go new file mode 100644 index 0000000..5968957 --- /dev/null +++ b/cmd/flag_values_test.go @@ -0,0 +1,33 @@ +package cmd + +import ( + "testing" + + "github.com/spf13/pflag" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestReadBoolFlag(t *testing.T) { + tests := []struct { + name string + setTo string + want BoolFlag + }{ + {name: "omitted", want: BoolFlag{}}, + {name: "true", setTo: "true", want: BoolFlag{Set: true, Value: true}}, + {name: "false", setTo: "false", want: BoolFlag{Set: true, Value: false}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + flags := pflag.NewFlagSet(t.Name(), pflag.ContinueOnError) + flags.Bool("record-session", false, "") + if tt.setTo != "" { + require.NoError(t, flags.Set("record-session", tt.setTo)) + } + + assert.Equal(t, tt.want, readBoolFlag(flags, "record-session")) + }) + } +}