diff --git a/.release-please-manifest.json b/.release-please-manifest.json index a26ebfc..54c4d98 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.14.0" + ".": "0.14.1" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 0af2575..f577dd0 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 51 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-8a6175a75caa75c3de5400edf97a34e526ac3f62c63955375437461581deb0c2.yml -openapi_spec_hash: 1a880e4ce337a0e44630e6d87ef5162a -config_hash: 49c2ff978aaa5ccb4ce324a72f116010 +configured_endpoints: 57 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-936db268b3dcae5d64bd5d590506d8134304ffcbf67389eb9b1555b3febfd4cb.yml +openapi_spec_hash: 145485087adf1b28c052bacb4df68462 +config_hash: 15cd063f8e308686ac71bf9ee9634625 diff --git a/CHANGELOG.md b/CHANGELOG.md index a2006a3..d5aaaec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.14.1 (2025-10-07) + +Full Changelog: [v0.14.0...v0.14.1](https://github.com/onkernel/kernel-go-sdk/compare/v0.14.0...v0.14.1) + +### Features + +* WIP browser extensions ([22323cd](https://github.com/onkernel/kernel-go-sdk/commit/22323cdf2376bf2016cf1ebd1384f4c1b5ca6752)) + ## 0.14.0 (2025-10-03) Full Changelog: [v0.13.0...v0.14.0](https://github.com/onkernel/kernel-go-sdk/compare/v0.13.0...v0.14.0) diff --git a/README.md b/README.md index df317d7..a8036ef 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Or to pin the version: ```sh -go get -u 'github.com/onkernel/kernel-go-sdk@v0.14.0' +go get -u 'github.com/onkernel/kernel-go-sdk@v0.14.1' ``` diff --git a/api.md b/api.md index 5e755f8..9093b83 100644 --- a/api.md +++ b/api.md @@ -75,6 +75,7 @@ Methods: - client.Browsers.List(ctx context.Context) ([]kernel.BrowserListResponse, error) - client.Browsers.Delete(ctx context.Context, body kernel.BrowserDeleteParams) error - client.Browsers.DeleteByID(ctx context.Context, id string) error +- client.Browsers.UploadExtensions(ctx context.Context, id string, body kernel.BrowserUploadExtensionsParams) error ## Replays @@ -175,3 +176,18 @@ Methods: - client.Proxies.Get(ctx context.Context, id string) (kernel.ProxyGetResponse, error) - client.Proxies.List(ctx context.Context) ([]kernel.ProxyListResponse, error) - client.Proxies.Delete(ctx context.Context, id string) error + +# Extensions + +Response Types: + +- kernel.ExtensionListResponse +- kernel.ExtensionUploadResponse + +Methods: + +- client.Extensions.List(ctx context.Context) ([]kernel.ExtensionListResponse, error) +- client.Extensions.Delete(ctx context.Context, idOrName string) error +- client.Extensions.Download(ctx context.Context, idOrName string) (http.Response, error) +- client.Extensions.DownloadFromChromeStore(ctx context.Context, query kernel.ExtensionDownloadFromChromeStoreParams) (http.Response, error) +- client.Extensions.Upload(ctx context.Context, body kernel.ExtensionUploadParams) (kernel.ExtensionUploadResponse, error) diff --git a/browser.go b/browser.go index afe9db9..1afa4ce 100644 --- a/browser.go +++ b/browser.go @@ -3,15 +3,19 @@ package kernel import ( + "bytes" "context" "encoding/json" "errors" "fmt" + "io" + "mime/multipart" "net/http" "net/url" "slices" "time" + "github.com/onkernel/kernel-go-sdk/internal/apiform" "github.com/onkernel/kernel-go-sdk/internal/apijson" "github.com/onkernel/kernel-go-sdk/internal/apiquery" "github.com/onkernel/kernel-go-sdk/internal/requestconfig" @@ -97,6 +101,20 @@ func (r *BrowserService) DeleteByID(ctx context.Context, id string, opts ...opti return } +// Loads one or more unpacked extensions and restarts Chromium on the browser +// instance. +func (r *BrowserService) UploadExtensions(ctx context.Context, id string, body BrowserUploadExtensionsParams, opts ...option.RequestOption) (err error) { + opts = slices.Concat(r.Options, opts) + opts = append([]option.RequestOption{option.WithHeader("Accept", "")}, opts...) + if id == "" { + err = errors.New("missing required id parameter") + return + } + path := fmt.Sprintf("browsers/%s/extensions", id) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, nil, opts...) + return +} + // Optional persistence configuration for the browser session. type BrowserPersistence struct { // Unique identifier for the persistent browser session. @@ -325,6 +343,8 @@ type BrowserNewParams struct { // seconds, so the actual timeout behavior you will see is +/- 5 seconds around the // specified value. TimeoutSeconds param.Opt[int64] `json:"timeout_seconds,omitzero"` + // List of browser extensions to load into the session. Provide each by id or name. + Extensions []BrowserNewParamsExtension `json:"extensions,omitzero"` // Optional persistence configuration for the browser session. Persistence BrowserPersistenceParam `json:"persistence,omitzero"` // Profile selection for the browser session. Provide either id or name. If @@ -342,6 +362,25 @@ func (r *BrowserNewParams) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } +// Extension selection for the browser session. Provide either id or name of an +// extension uploaded to Kernel. +type BrowserNewParamsExtension struct { + // Extension ID to load for this browser session + ID param.Opt[string] `json:"id,omitzero"` + // Extension name to load for this browser session (instead of id). Must be 1-255 + // characters, using letters, numbers, dots, underscores, or hyphens. + Name param.Opt[string] `json:"name,omitzero"` + paramObj +} + +func (r BrowserNewParamsExtension) MarshalJSON() (data []byte, err error) { + type shadow BrowserNewParamsExtension + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *BrowserNewParamsExtension) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + // Profile selection for the browser session. Provide either id or name. If // specified, the matching profile will be loaded into the browser session. // Profiles must be created beforehand. @@ -378,3 +417,45 @@ func (r BrowserDeleteParams) URLQuery() (v url.Values, err error) { NestedFormat: apiquery.NestedQueryFormatBrackets, }) } + +type BrowserUploadExtensionsParams struct { + // List of extensions to upload and activate + Extensions []BrowserUploadExtensionsParamsExtension `json:"extensions,omitzero,required"` + paramObj +} + +func (r BrowserUploadExtensionsParams) MarshalMultipart() (data []byte, contentType string, err error) { + buf := bytes.NewBuffer(nil) + writer := multipart.NewWriter(buf) + err = apiform.MarshalRoot(r, writer) + if err == nil { + err = apiform.WriteExtras(writer, r.ExtraFields()) + } + if err != nil { + writer.Close() + return nil, "", err + } + err = writer.Close() + if err != nil { + return nil, "", err + } + return buf.Bytes(), writer.FormDataContentType(), nil +} + +// The properties Name, ZipFile are required. +type BrowserUploadExtensionsParamsExtension struct { + // Folder name to place the extension under /home/kernel/extensions/ + Name string `json:"name,required"` + // Zip archive containing an unpacked Chromium extension (must include + // manifest.json) + ZipFile io.Reader `json:"zip_file,omitzero,required" format:"binary"` + paramObj +} + +func (r BrowserUploadExtensionsParamsExtension) MarshalJSON() (data []byte, err error) { + type shadow BrowserUploadExtensionsParamsExtension + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *BrowserUploadExtensionsParamsExtension) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} diff --git a/browser_test.go b/browser_test.go index 0c0c4e3..1817275 100644 --- a/browser_test.go +++ b/browser_test.go @@ -3,8 +3,10 @@ package kernel_test import ( + "bytes" "context" "errors" + "io" "os" "testing" @@ -27,6 +29,10 @@ func TestBrowserNewWithOptionalParams(t *testing.T) { option.WithAPIKey("My API Key"), ) _, err := client.Browsers.New(context.TODO(), kernel.BrowserNewParams{ + Extensions: []kernel.BrowserNewParamsExtension{{ + ID: kernel.String("id"), + Name: kernel.String("name"), + }}, Headless: kernel.Bool(false), InvocationID: kernel.String("rr33xuugxj9h0bkf1rdt2bet"), Persistence: kernel.BrowserPersistenceParam{ @@ -143,3 +149,35 @@ func TestBrowserDeleteByID(t *testing.T) { t.Fatalf("err should be nil: %s", err.Error()) } } + +func TestBrowserUploadExtensions(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := kernel.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + err := client.Browsers.UploadExtensions( + context.TODO(), + "id", + kernel.BrowserUploadExtensionsParams{ + Extensions: []kernel.BrowserUploadExtensionsParamsExtension{{ + Name: "name", + ZipFile: io.Reader(bytes.NewBuffer([]byte("some file contents"))), + }}, + }, + ) + if err != nil { + var apierr *kernel.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} diff --git a/client.go b/client.go index ec155ff..0e27e7b 100644 --- a/client.go +++ b/client.go @@ -23,6 +23,7 @@ type Client struct { Browsers BrowserService Profiles ProfileService Proxies ProxyService + Extensions ExtensionService } // DefaultClientOptions read from the environment (KERNEL_API_KEY, @@ -53,6 +54,7 @@ func NewClient(opts ...option.RequestOption) (r Client) { r.Browsers = NewBrowserService(opts...) r.Profiles = NewProfileService(opts...) r.Proxies = NewProxyService(opts...) + r.Extensions = NewExtensionService(opts...) return } diff --git a/extension.go b/extension.go new file mode 100644 index 0000000..204b126 --- /dev/null +++ b/extension.go @@ -0,0 +1,212 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package kernel + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "mime/multipart" + "net/http" + "net/url" + "slices" + "time" + + "github.com/onkernel/kernel-go-sdk/internal/apiform" + "github.com/onkernel/kernel-go-sdk/internal/apijson" + "github.com/onkernel/kernel-go-sdk/internal/apiquery" + "github.com/onkernel/kernel-go-sdk/internal/requestconfig" + "github.com/onkernel/kernel-go-sdk/option" + "github.com/onkernel/kernel-go-sdk/packages/param" + "github.com/onkernel/kernel-go-sdk/packages/respjson" +) + +// ExtensionService contains methods and other services that help with interacting +// with the kernel API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewExtensionService] method instead. +type ExtensionService struct { + Options []option.RequestOption +} + +// NewExtensionService generates a new service that applies the given options to +// each request. These options are applied after the parent client's options (if +// there is one), and before any request-specific options. +func NewExtensionService(opts ...option.RequestOption) (r ExtensionService) { + r = ExtensionService{} + r.Options = opts + return +} + +// List extensions owned by the caller's organization. +func (r *ExtensionService) List(ctx context.Context, opts ...option.RequestOption) (res *[]ExtensionListResponse, err error) { + opts = slices.Concat(r.Options, opts) + path := "extensions" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + return +} + +// Delete an extension by its ID or by its name. +func (r *ExtensionService) Delete(ctx context.Context, idOrName string, opts ...option.RequestOption) (err error) { + opts = slices.Concat(r.Options, opts) + opts = append([]option.RequestOption{option.WithHeader("Accept", "")}, opts...) + if idOrName == "" { + err = errors.New("missing required id_or_name parameter") + return + } + path := fmt.Sprintf("extensions/%s", idOrName) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, nil, opts...) + return +} + +// Download the extension as a ZIP archive by ID or name. +func (r *ExtensionService) Download(ctx context.Context, idOrName string, opts ...option.RequestOption) (res *http.Response, err error) { + opts = slices.Concat(r.Options, opts) + opts = append([]option.RequestOption{option.WithHeader("Accept", "application/octet-stream")}, opts...) + if idOrName == "" { + err = errors.New("missing required id_or_name parameter") + return + } + path := fmt.Sprintf("extensions/%s", idOrName) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + return +} + +// Returns a ZIP archive containing the unpacked extension fetched from the Chrome +// Web Store. +func (r *ExtensionService) DownloadFromChromeStore(ctx context.Context, query ExtensionDownloadFromChromeStoreParams, opts ...option.RequestOption) (res *http.Response, err error) { + opts = slices.Concat(r.Options, opts) + opts = append([]option.RequestOption{option.WithHeader("Accept", "application/octet-stream")}, opts...) + path := "extensions/from_chrome_store" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) + return +} + +// Upload a zip file containing an unpacked browser extension. Optionally provide a +// unique name for later reference. +func (r *ExtensionService) Upload(ctx context.Context, body ExtensionUploadParams, opts ...option.RequestOption) (res *ExtensionUploadResponse, err error) { + opts = slices.Concat(r.Options, opts) + path := "extensions" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return +} + +// A browser extension uploaded to Kernel. +type ExtensionListResponse struct { + // Unique identifier for the extension + ID string `json:"id,required"` + // Timestamp when the extension was created + CreatedAt time.Time `json:"created_at,required" format:"date-time"` + // Size of the extension archive in bytes + SizeBytes int64 `json:"size_bytes,required"` + // Timestamp when the extension was last used + LastUsedAt time.Time `json:"last_used_at,nullable" format:"date-time"` + // Optional, easier-to-reference name for the extension. Must be unique within the + // organization. + Name string `json:"name,nullable"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ID respjson.Field + CreatedAt respjson.Field + SizeBytes respjson.Field + LastUsedAt respjson.Field + Name respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r ExtensionListResponse) RawJSON() string { return r.JSON.raw } +func (r *ExtensionListResponse) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// A browser extension uploaded to Kernel. +type ExtensionUploadResponse struct { + // Unique identifier for the extension + ID string `json:"id,required"` + // Timestamp when the extension was created + CreatedAt time.Time `json:"created_at,required" format:"date-time"` + // Size of the extension archive in bytes + SizeBytes int64 `json:"size_bytes,required"` + // Timestamp when the extension was last used + LastUsedAt time.Time `json:"last_used_at,nullable" format:"date-time"` + // Optional, easier-to-reference name for the extension. Must be unique within the + // organization. + Name string `json:"name,nullable"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ID respjson.Field + CreatedAt respjson.Field + SizeBytes respjson.Field + LastUsedAt respjson.Field + Name respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r ExtensionUploadResponse) RawJSON() string { return r.JSON.raw } +func (r *ExtensionUploadResponse) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type ExtensionDownloadFromChromeStoreParams struct { + // Chrome Web Store URL for the extension. + URL string `query:"url,required" json:"-"` + // Target operating system for the extension package. Defaults to linux. + // + // Any of "win", "mac", "linux". + Os ExtensionDownloadFromChromeStoreParamsOs `query:"os,omitzero" json:"-"` + paramObj +} + +// URLQuery serializes [ExtensionDownloadFromChromeStoreParams]'s query parameters +// as `url.Values`. +func (r ExtensionDownloadFromChromeStoreParams) URLQuery() (v url.Values, err error) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + +// Target operating system for the extension package. Defaults to linux. +type ExtensionDownloadFromChromeStoreParamsOs string + +const ( + ExtensionDownloadFromChromeStoreParamsOsWin ExtensionDownloadFromChromeStoreParamsOs = "win" + ExtensionDownloadFromChromeStoreParamsOsMac ExtensionDownloadFromChromeStoreParamsOs = "mac" + ExtensionDownloadFromChromeStoreParamsOsLinux ExtensionDownloadFromChromeStoreParamsOs = "linux" +) + +type ExtensionUploadParams struct { + // ZIP file containing the browser extension. + File io.Reader `json:"file,omitzero,required" format:"binary"` + // Optional unique name within the organization to reference this extension. + Name param.Opt[string] `json:"name,omitzero"` + paramObj +} + +func (r ExtensionUploadParams) MarshalMultipart() (data []byte, contentType string, err error) { + buf := bytes.NewBuffer(nil) + writer := multipart.NewWriter(buf) + err = apiform.MarshalRoot(r, writer) + if err == nil { + err = apiform.WriteExtras(writer, r.ExtraFields()) + } + if err != nil { + writer.Close() + return nil, "", err + } + err = writer.Close() + if err != nil { + return nil, "", err + } + return buf.Bytes(), writer.FormDataContentType(), nil +} diff --git a/extension_test.go b/extension_test.go new file mode 100644 index 0000000..af2089a --- /dev/null +++ b/extension_test.go @@ -0,0 +1,161 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package kernel_test + +import ( + "bytes" + "context" + "errors" + "io" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/onkernel/kernel-go-sdk" + "github.com/onkernel/kernel-go-sdk/internal/testutil" + "github.com/onkernel/kernel-go-sdk/option" +) + +func TestExtensionList(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := kernel.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + _, err := client.Extensions.List(context.TODO()) + if err != nil { + var apierr *kernel.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestExtensionDelete(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := kernel.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + err := client.Extensions.Delete(context.TODO(), "id_or_name") + if err != nil { + var apierr *kernel.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestExtensionDownload(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Write([]byte("abc")) + })) + defer server.Close() + baseURL := server.URL + client := kernel.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + resp, err := client.Extensions.Download(context.TODO(), "id_or_name") + if err != nil { + var apierr *kernel.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } + defer resp.Body.Close() + + b, err := io.ReadAll(resp.Body) + if err != nil { + var apierr *kernel.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } + if !bytes.Equal(b, []byte("abc")) { + t.Fatalf("return value not %s: %s", "abc", b) + } +} + +func TestExtensionDownloadFromChromeStoreWithOptionalParams(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Write([]byte("abc")) + })) + defer server.Close() + baseURL := server.URL + client := kernel.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + resp, err := client.Extensions.DownloadFromChromeStore(context.TODO(), kernel.ExtensionDownloadFromChromeStoreParams{ + URL: "url", + Os: kernel.ExtensionDownloadFromChromeStoreParamsOsWin, + }) + if err != nil { + var apierr *kernel.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } + defer resp.Body.Close() + + b, err := io.ReadAll(resp.Body) + if err != nil { + var apierr *kernel.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } + if !bytes.Equal(b, []byte("abc")) { + t.Fatalf("return value not %s: %s", "abc", b) + } +} + +func TestExtensionUploadWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := kernel.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + _, err := client.Extensions.Upload(context.TODO(), kernel.ExtensionUploadParams{ + File: io.Reader(bytes.NewBuffer([]byte("some file contents"))), + Name: kernel.String("name"), + }) + if err != nil { + var apierr *kernel.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} diff --git a/internal/version.go b/internal/version.go index 870e575..15be922 100644 --- a/internal/version.go +++ b/internal/version.go @@ -2,4 +2,4 @@ package internal -const PackageVersion = "0.14.0" // x-release-please-version +const PackageVersion = "0.14.1" // x-release-please-version