From 3095c2bcd1f39858781aded2a0a85293868fb223 Mon Sep 17 00:00:00 2001 From: Ulzii Otgonbaatar Date: Thu, 30 Jul 2026 09:19:03 -0600 Subject: [PATCH 1/2] Support --asn for ISP proxies --asn was registered on every proxy type but only wired into residential and mobile, so passing it with --type isp silently dropped it and returned a proxy on an arbitrary network. Wires --asn into the ISP config, and makes the types that cannot honour it (datacenter, custom, mobile) fail with a clear error instead of ignoring it. Mobile previously warned and continued for --zip/--asn, which the API rejects anyway, so both are now errors. Requires the SDK bump that adds Asn to ProxyNewParamsConfigIsp. Co-authored-by: Cursor --- README.md | 5 +- cmd/proxies/create.go | 16 ++++- cmd/proxies/create_asn_test.go | 104 +++++++++++++++++++++++++++++++++ cmd/proxies/proxies.go | 2 +- 4 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 cmd/proxies/create_asn_test.go diff --git a/README.md b/README.md index c54f458..101b06e 100644 --- a/README.md +++ b/README.md @@ -510,7 +510,7 @@ Per-category updates are partial — only categories you name are changed; other - `--city ` - City name (no spaces, e.g. sanfrancisco) (residential, mobile; requires `--country`) - `--state ` - Two-letter state code (residential, mobile) - `--zip ` - US ZIP code (residential) - - `--asn ` - Autonomous system number (e.g., AS15169) (residential) + - `--asn ` - Autonomous system number, e.g. AS6079 (isp, residential). The ISP pool is a fixed set of static IPs, so only ASNs present in it can be requested; an unsupported value is rejected with the list of available ASNs. - `--os ` - Operating system: windows, macos, android (residential) - `--host ` - Proxy host (custom; required) - `--port ` - Proxy port (custom; required) @@ -880,6 +880,9 @@ kernel proxies create --type custom --host proxy.example.com --port 8080 --ca-bu # 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" +# Create an ISP proxy pinned to a specific ASN +kernel proxies create --type isp --asn AS6079 --name "RCN ISP" + # Create a mobile proxy kernel proxies create --type mobile --country US --city sanfrancisco --name "US Mobile" diff --git a/cmd/proxies/create.go b/cmd/proxies/create.go index cd5011c..eac6777 100644 --- a/cmd/proxies/create.go +++ b/cmd/proxies/create.go @@ -49,6 +49,9 @@ func (p ProxyCmd) Create(ctx context.Context, in ProxyCreateInput) error { // Build config based on type switch proxyType { case kernel.ProxyNewParamsTypeDatacenter: + if in.ASN != "" { + return fmt.Errorf("--asn is not supported for datacenter proxies") + } config := kernel.ProxyNewParamsConfigDatacenter{} if in.Country != "" { config.Country = kernel.Opt(in.Country) @@ -62,6 +65,9 @@ func (p ProxyCmd) Create(ctx context.Context, in ProxyCreateInput) error { if in.Country != "" { config.Country = kernel.Opt(in.Country) } + if in.ASN != "" { + config.Asn = kernel.Opt(in.ASN) + } params.Config = kernel.ProxyNewParamsConfigUnion{ OfIsp: &config, } @@ -109,8 +115,11 @@ func (p ProxyCmd) Create(ctx context.Context, in ProxyCreateInput) error { if in.City != "" && in.Country == "" { return fmt.Errorf("--country is required when --city is specified") } - if in.Zip != "" || in.ASN != "" { - pterm.Warning.Println("--zip and --asn are not supported for mobile proxies and will be ignored") + if in.Zip != "" { + return fmt.Errorf("--zip is not supported for mobile proxies") + } + if in.ASN != "" { + return fmt.Errorf("--asn is not supported for mobile proxies") } if in.Country != "" { @@ -127,6 +136,9 @@ func (p ProxyCmd) Create(ctx context.Context, in ProxyCreateInput) error { } case kernel.ProxyNewParamsTypeCustom: + if in.ASN != "" { + return fmt.Errorf("--asn is not supported for custom proxies; the upstream proxy determines the egress network") + } if in.Host == "" { return fmt.Errorf("--host is required for custom proxy type") } diff --git a/cmd/proxies/create_asn_test.go b/cmd/proxies/create_asn_test.go new file mode 100644 index 0000000..e09e828 --- /dev/null +++ b/cmd/proxies/create_asn_test.go @@ -0,0 +1,104 @@ +package proxies + +import ( + "context" + "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_ISP_SendsASN(t *testing.T) { + captureOutput(t) + + called := false + fake := &FakeProxyService{ + NewFunc: func(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (*kernel.ProxyNewResponse, error) { + called = true + ispConfig := body.Config.OfIsp + require.NotNil(t, ispConfig) + assert.Equal(t, "AS6079", ispConfig.Asn.Value) + + return &kernel.ProxyNewResponse{ID: "isp-new", Name: "RCN ISP", Type: kernel.ProxyNewResponseTypeIsp}, nil + }, + } + + p := ProxyCmd{proxies: fake} + err := p.Create(context.Background(), ProxyCreateInput{ + Name: "RCN ISP", + Type: "isp", + ASN: "AS6079", + }) + + require.NoError(t, err) + assert.True(t, called, "expected the create request to be sent") +} + +// Omitting --asn must not start sending an empty ASN, which the API would reject as +// malformed rather than treating as "no preference". +func TestProxyCreate_ISP_OmitsEmptyASN(t *testing.T) { + captureOutput(t) + + fake := &FakeProxyService{ + NewFunc: func(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (*kernel.ProxyNewResponse, error) { + ispConfig := body.Config.OfIsp + require.NotNil(t, ispConfig) + assert.False(t, ispConfig.Asn.Valid(), "asn should be absent when not requested") + + return &kernel.ProxyNewResponse{ID: "isp-new", Name: "Any ISP", Type: kernel.ProxyNewResponseTypeIsp}, nil + }, + } + + p := ProxyCmd{proxies: fake} + err := p.Create(context.Background(), ProxyCreateInput{Name: "Any ISP", Type: "isp"}) + require.NoError(t, err) +} + +// The flag is registered on every type, so the types that cannot honour it have to say so +// rather than drop it and hand back a proxy on an arbitrary network. +func TestProxyCreate_ASNRejectedForUnsupportedTypes(t *testing.T) { + tests := []struct { + proxyType string + input ProxyCreateInput + }{ + {"datacenter", ProxyCreateInput{Name: "dc", Type: "datacenter", ASN: "AS6079"}}, + {"mobile", ProxyCreateInput{Name: "mob", Type: "mobile", Country: "US", ASN: "AS6079"}}, + {"custom", ProxyCreateInput{Name: "cus", Type: "custom", Host: "proxy.example.com", Port: 8080, ASN: "AS6079"}}, + } + + for _, tt := range tests { + t.Run(tt.proxyType, func(t *testing.T) { + captureOutput(t) + + fake := &FakeProxyService{ + NewFunc: func(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (*kernel.ProxyNewResponse, error) { + require.Failf(t, "unexpected request", "create should not be attempted for %s with --asn", tt.proxyType) + return nil, nil + }, + } + + p := ProxyCmd{proxies: fake} + err := p.Create(context.Background(), tt.input) + require.Error(t, err) + assert.Contains(t, err.Error(), "--asn is not supported") + }) + } +} + +func TestProxyCreate_ZipRejectedForMobile(t *testing.T) { + captureOutput(t) + + fake := &FakeProxyService{ + NewFunc: func(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (*kernel.ProxyNewResponse, error) { + require.Fail(t, "unexpected request", "create should not be attempted for mobile with --zip") + return nil, nil + }, + } + + p := ProxyCmd{proxies: fake} + err := p.Create(context.Background(), ProxyCreateInput{Name: "mob", Type: "mobile", Country: "US", Zip: "94107"}) + require.Error(t, err) + assert.Contains(t, err.Error(), "--zip is not supported") +} diff --git a/cmd/proxies/proxies.go b/cmd/proxies/proxies.go index 7642001..e707afd 100644 --- a/cmd/proxies/proxies.go +++ b/cmd/proxies/proxies.go @@ -110,7 +110,7 @@ func init() { proxiesCreateCmd.Flags().String("city", "", "City name (no spaces, e.g. sanfrancisco)") proxiesCreateCmd.Flags().String("state", "", "Two-letter state code") proxiesCreateCmd.Flags().String("zip", "", "US ZIP code") - proxiesCreateCmd.Flags().String("asn", "", "Autonomous system number (e.g. AS15169)") + proxiesCreateCmd.Flags().String("asn", "", "Autonomous system number, e.g. AS6079 (isp, residential)") // OS flag (residential) proxiesCreateCmd.Flags().String("os", "", "Operating system (windows|macos|android)") From ac173c599e35cf0d950129e9cac785828b290c1f Mon Sep 17 00:00:00 2001 From: Ulzii Otgonbaatar Date: Thu, 30 Jul 2026 09:45:53 -0600 Subject: [PATCH 2/2] Send the ISP ASN as an extra field until the SDK is regenerated The generated ProxyNewParamsConfigIsp has no Asn field, so the previous commit did not build against the pinned SDK. Send asn via SetExtraFields, which produces the same request body, so the flag works against the deployed API without waiting on a Stainless release. Tests now assert on the serialized request rather than the struct field, so they keep verifying the wire format once asn becomes a generated field. Co-authored-by: Cursor --- cmd/proxies/create.go | 5 +++- cmd/proxies/create_asn_test.go | 49 +++++++++++++++++++++++++++++----- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/cmd/proxies/create.go b/cmd/proxies/create.go index eac6777..cba793f 100644 --- a/cmd/proxies/create.go +++ b/cmd/proxies/create.go @@ -66,7 +66,10 @@ func (p ProxyCmd) Create(ctx context.Context, in ProxyCreateInput) error { config.Country = kernel.Opt(in.Country) } if in.ASN != "" { - config.Asn = kernel.Opt(in.ASN) + // The generated ISP config has no Asn field until Stainless regenerates + // against the spec. Send it as an extra field so the flag works against + // the deployed API now; replace with config.Asn on the next SDK bump. + config.SetExtraFields(map[string]any{"asn": in.ASN}) } params.Config = kernel.ProxyNewParamsConfigUnion{ OfIsp: &config, diff --git a/cmd/proxies/create_asn_test.go b/cmd/proxies/create_asn_test.go index e09e828..d38d4b5 100644 --- a/cmd/proxies/create_asn_test.go +++ b/cmd/proxies/create_asn_test.go @@ -2,6 +2,7 @@ package proxies import ( "context" + "encoding/json" "testing" "github.com/kernel/kernel-go-sdk" @@ -10,6 +11,22 @@ import ( "github.com/stretchr/testify/require" ) +// The ASN currently travels as an extra field rather than a generated one, so assert on +// the serialized request: that is what the API sees, and it stays valid once the field +// becomes typed. +func ispConfigJSON(t *testing.T, body kernel.ProxyNewParams) map[string]any { + t.Helper() + + raw, err := json.Marshal(body) + require.NoError(t, err) + + var decoded struct { + Config map[string]any `json:"config"` + } + require.NoError(t, json.Unmarshal(raw, &decoded)) + return decoded.Config +} + func TestProxyCreate_ISP_SendsASN(t *testing.T) { captureOutput(t) @@ -17,9 +34,7 @@ func TestProxyCreate_ISP_SendsASN(t *testing.T) { fake := &FakeProxyService{ NewFunc: func(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (*kernel.ProxyNewResponse, error) { called = true - ispConfig := body.Config.OfIsp - require.NotNil(t, ispConfig) - assert.Equal(t, "AS6079", ispConfig.Asn.Value) + assert.Equal(t, "AS6079", ispConfigJSON(t, body)["asn"]) return &kernel.ProxyNewResponse{ID: "isp-new", Name: "RCN ISP", Type: kernel.ProxyNewResponseTypeIsp}, nil }, @@ -36,6 +51,29 @@ func TestProxyCreate_ISP_SendsASN(t *testing.T) { assert.True(t, called, "expected the create request to be sent") } +func TestProxyCreate_ISP_SendsASNAlongsideCountry(t *testing.T) { + captureOutput(t) + + fake := &FakeProxyService{ + NewFunc: func(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (*kernel.ProxyNewResponse, error) { + config := ispConfigJSON(t, body) + assert.Equal(t, "AS6079", config["asn"]) + assert.Equal(t, "US", config["country"], "the extra field must not displace country") + + return &kernel.ProxyNewResponse{ID: "isp-new", Name: "RCN ISP", Type: kernel.ProxyNewResponseTypeIsp}, nil + }, + } + + p := ProxyCmd{proxies: fake} + err := p.Create(context.Background(), ProxyCreateInput{ + Name: "RCN ISP", + Type: "isp", + Country: "US", + ASN: "AS6079", + }) + require.NoError(t, err) +} + // Omitting --asn must not start sending an empty ASN, which the API would reject as // malformed rather than treating as "no preference". func TestProxyCreate_ISP_OmitsEmptyASN(t *testing.T) { @@ -43,9 +81,8 @@ func TestProxyCreate_ISP_OmitsEmptyASN(t *testing.T) { fake := &FakeProxyService{ NewFunc: func(ctx context.Context, body kernel.ProxyNewParams, opts ...option.RequestOption) (*kernel.ProxyNewResponse, error) { - ispConfig := body.Config.OfIsp - require.NotNil(t, ispConfig) - assert.False(t, ispConfig.Asn.Valid(), "asn should be absent when not requested") + _, present := ispConfigJSON(t, body)["asn"] + assert.False(t, present, "asn should be absent when not requested") return &kernel.ProxyNewResponse{ID: "isp-new", Name: "Any ISP", Type: kernel.ProxyNewResponseTypeIsp}, nil },