From e7f9fd8aa890b660d675b22ad48f869504817b2d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:08:34 +0000 Subject: [PATCH 1/6] feat(api): surface deleted/expired API keys for audit trail (KERNEL-1350) --- .stats.yml | 4 ++-- api.md | 2 +- apikey.go | 44 ++++++++++++++++++++++++++++++++++++++++++-- apikey_test.go | 21 ++++++++++++++------- 4 files changed, 59 insertions(+), 12 deletions(-) diff --git a/.stats.yml b/.stats.yml index a164009..3ff52d9 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 119 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-51549f813f3002e18c6ca8d850cc0c7932828d511c151e0412c73b6798d19e30.yml -openapi_spec_hash: ee77b293c4bda91c1a32cfdd12b8739e +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-42074f2b600b0dc805377d6793e4bb30c959738b0f9cc44c409d094517e5e0ab.yml +openapi_spec_hash: 81c27a833d6d9637787634180dec2abd config_hash: 57567e00b41af47cef1b78e51b747aa0 diff --git a/api.md b/api.md index f4a6c67..448334f 100644 --- a/api.md +++ b/api.md @@ -427,7 +427,7 @@ Response Types: Methods: - client.APIKeys.New(ctx context.Context, body kernel.APIKeyNewParams) (\*kernel.CreatedAPIKey, error) -- client.APIKeys.Get(ctx context.Context, id string) (\*kernel.APIKey, error) +- client.APIKeys.Get(ctx context.Context, id string, query kernel.APIKeyGetParams) (\*kernel.APIKey, error) - client.APIKeys.Update(ctx context.Context, id string, body kernel.APIKeyUpdateParams) (\*kernel.APIKey, error) - client.APIKeys.List(ctx context.Context, query kernel.APIKeyListParams) (\*pagination.OffsetPagination[kernel.APIKey], error) - client.APIKeys.Delete(ctx context.Context, id string) error diff --git a/apikey.go b/apikey.go index 1fed55a..0cde722 100644 --- a/apikey.go +++ b/apikey.go @@ -51,14 +51,14 @@ func (r *APIKeyService) New(ctx context.Context, body APIKeyNewParams, opts ...o // Retrieve an API key by ID for the authenticated organization. API keys are // masked. -func (r *APIKeyService) Get(ctx context.Context, id string, opts ...option.RequestOption) (res *APIKey, err error) { +func (r *APIKeyService) Get(ctx context.Context, id string, query APIKeyGetParams, opts ...option.RequestOption) (res *APIKey, err error) { opts = slices.Concat(r.Options, opts) if id == "" { err = errors.New("missing required id parameter") return nil, err } path := fmt.Sprintf("org/api_keys/%s", id) - err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) return res, err } @@ -116,6 +116,9 @@ type APIKey struct { // When the API key was created CreatedAt time.Time `json:"created_at" api:"required" format:"date-time"` CreatedBy APIKeyCreatedBy `json:"created_by" api:"required"` + // When the API key was deleted (soft-deleted). Null for keys that have not been + // deleted. + DeletedAt time.Time `json:"deleted_at" api:"required" format:"date-time"` // When the API key expires ExpiresAt time.Time `json:"expires_at" api:"required" format:"date-time"` // Masked version of the API key @@ -127,16 +130,24 @@ type APIKey struct { // Project name for project-scoped API keys. Null means the key is org-wide or the // project name is unavailable. ProjectName string `json:"project_name" api:"required"` + // Derived lifecycle status of the API key. `active` means usable. `expired` means + // past its expires_at. `deleted` means it was deleted (soft-deleted) and can no + // longer authenticate. Deleted takes precedence over expired. + // + // Any of "active", "expired", "deleted". + Status APIKeyStatus `json:"status" api:"required"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { ID respjson.Field CreatedAt respjson.Field CreatedBy respjson.Field + DeletedAt respjson.Field ExpiresAt respjson.Field MaskedKey respjson.Field Name respjson.Field ProjectID respjson.Field ProjectName respjson.Field + Status respjson.Field ExtraFields map[string]respjson.Field raw string } `json:"-"` @@ -171,6 +182,17 @@ func (r *APIKeyCreatedBy) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } +// Derived lifecycle status of the API key. `active` means usable. `expired` means +// past its expires_at. `deleted` means it was deleted (soft-deleted) and can no +// longer authenticate. Deleted takes precedence over expired. +type APIKeyStatus string + +const ( + APIKeyStatusActive APIKeyStatus = "active" + APIKeyStatusExpired APIKeyStatus = "expired" + APIKeyStatusDeleted APIKeyStatus = "deleted" +) + // API key returned immediately after creation. Includes the plaintext key once. type CreatedAPIKey struct { // Plaintext API key. Only returned once when the key is created. @@ -208,6 +230,21 @@ func (r *APIKeyNewParams) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } +type APIKeyGetParams struct { + // When true, return the API key even if it has been deleted (soft-deleted), for + // audit purposes. Defaults to false, which returns 404 for a deleted key. + IncludeDeleted param.Opt[bool] `query:"include_deleted,omitzero" json:"-"` + paramObj +} + +// URLQuery serializes [APIKeyGetParams]'s query parameters as `url.Values`. +func (r APIKeyGetParams) URLQuery() (v url.Values, err error) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + type APIKeyUpdateParams struct { // New API key name Name string `json:"name" api:"required"` @@ -223,6 +260,9 @@ func (r *APIKeyUpdateParams) UnmarshalJSON(data []byte) error { } type APIKeyListParams struct { + // When true, include deleted (soft-deleted) API keys in the results for audit + // purposes. Defaults to false, which returns only live keys. + IncludeDeleted param.Opt[bool] `query:"include_deleted,omitzero" json:"-"` // Maximum number of results to return Limit param.Opt[int64] `query:"limit,omitzero" json:"-"` // Number of results to skip diff --git a/apikey_test.go b/apikey_test.go index 946b46b..6d19cf5 100644 --- a/apikey_test.go +++ b/apikey_test.go @@ -40,7 +40,7 @@ func TestAPIKeyNewWithOptionalParams(t *testing.T) { } } -func TestAPIKeyGet(t *testing.T) { +func TestAPIKeyGetWithOptionalParams(t *testing.T) { t.Skip("Mock server tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { @@ -53,7 +53,13 @@ func TestAPIKeyGet(t *testing.T) { option.WithBaseURL(baseURL), option.WithAPIKey("My API Key"), ) - _, err := client.APIKeys.Get(context.TODO(), "id") + _, err := client.APIKeys.Get( + context.TODO(), + "id", + kernel.APIKeyGetParams{ + IncludeDeleted: kernel.Bool(true), + }, + ) if err != nil { var apierr *kernel.Error if errors.As(err, &apierr) { @@ -106,11 +112,12 @@ func TestAPIKeyListWithOptionalParams(t *testing.T) { option.WithAPIKey("My API Key"), ) _, err := client.APIKeys.List(context.TODO(), kernel.APIKeyListParams{ - Limit: kernel.Int(100), - Offset: kernel.Int(0), - Query: kernel.String("query"), - SortBy: kernel.APIKeyListParamsSortByCreatedAt, - SortDirection: kernel.APIKeyListParamsSortDirectionAsc, + IncludeDeleted: kernel.Bool(true), + Limit: kernel.Int(100), + Offset: kernel.Int(0), + Query: kernel.String("query"), + SortBy: kernel.APIKeyListParamsSortByCreatedAt, + SortDirection: kernel.APIKeyListParamsSortDirectionAsc, }) if err != nil { var apierr *kernel.Error From 50dc5fc0b757472829733c430f62fed5bfd64cf8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 12 Jun 2026 19:04:22 +0000 Subject: [PATCH 2/6] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 3ff52d9..ab902c2 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 119 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-42074f2b600b0dc805377d6793e4bb30c959738b0f9cc44c409d094517e5e0ab.yml -openapi_spec_hash: 81c27a833d6d9637787634180dec2abd +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-c98841235b0ece0591f28f7dd424339b6ef2f3e8f539b95b670ae0da2ef43df4.yml +openapi_spec_hash: c1e9456765f0743a333af297d135d5cf config_hash: 57567e00b41af47cef1b78e51b747aa0 From 04d4a12f1f98a5745b7ca706a22cab71cb625866 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 14 Jun 2026 19:08:51 +0000 Subject: [PATCH 3/6] feat: Add API key rotate endpoint --- .stats.yml | 8 ++++---- api.md | 1 + apikey.go | 32 ++++++++++++++++++++++++++++++++ apikey_test.go | 30 ++++++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 4 deletions(-) diff --git a/.stats.yml b/.stats.yml index ab902c2..8d8fe88 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 119 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-c98841235b0ece0591f28f7dd424339b6ef2f3e8f539b95b670ae0da2ef43df4.yml -openapi_spec_hash: c1e9456765f0743a333af297d135d5cf -config_hash: 57567e00b41af47cef1b78e51b747aa0 +configured_endpoints: 120 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-1f10598afa01d76d22b0ed63685248f482f74c9353cffe1d3e4a3d38da7716cf.yml +openapi_spec_hash: 8936f458bfa681b709e459ca1cc76fb5 +config_hash: 03c7e57f268c750e2415831662e95969 diff --git a/api.md b/api.md index 448334f..0c51245 100644 --- a/api.md +++ b/api.md @@ -431,6 +431,7 @@ Methods: - client.APIKeys.Update(ctx context.Context, id string, body kernel.APIKeyUpdateParams) (\*kernel.APIKey, error) - client.APIKeys.List(ctx context.Context, query kernel.APIKeyListParams) (\*pagination.OffsetPagination[kernel.APIKey], error) - client.APIKeys.Delete(ctx context.Context, id string) error +- client.APIKeys.Rotate(ctx context.Context, id string, body kernel.APIKeyRotateParams) (\*kernel.CreatedAPIKey, error) # CredentialProviders diff --git a/apikey.go b/apikey.go index 0cde722..0b08caa 100644 --- a/apikey.go +++ b/apikey.go @@ -110,6 +110,20 @@ func (r *APIKeyService) Delete(ctx context.Context, id string, opts ...option.Re return err } +// Rotate an API key. Issues a new key that copies the name and project of the +// rotated key, and schedules the rotated key to expire after a grace period so +// in-flight callers can swap over. The new plaintext key is returned once. +func (r *APIKeyService) Rotate(ctx context.Context, id string, body APIKeyRotateParams, opts ...option.RequestOption) (res *CreatedAPIKey, err error) { + opts = slices.Concat(r.Options, opts) + if id == "" { + err = errors.New("missing required id parameter") + return nil, err + } + path := fmt.Sprintf("org/api_keys/%s/rotate", id) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return res, err +} + type APIKey struct { // Unique API key identifier ID string `json:"id" api:"required"` @@ -305,3 +319,21 @@ const ( APIKeyListParamsSortDirectionAsc APIKeyListParamsSortDirection = "asc" APIKeyListParamsSortDirectionDesc APIKeyListParamsSortDirection = "desc" ) + +type APIKeyRotateParams struct { + // Lifetime in days for the new key, up to 3650. Omit to reuse the rotated key's + // original lifetime, or never-expires if it had none. + DaysToExpire param.Opt[int64] `json:"days_to_expire,omitzero"` + // Grace period in days before the rotated key expires. Use 0 to expire it + // immediately. Omit for the default grace period of 7 days. + ExpireInDays param.Opt[int64] `json:"expire_in_days,omitzero"` + paramObj +} + +func (r APIKeyRotateParams) MarshalJSON() (data []byte, err error) { + type shadow APIKeyRotateParams + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *APIKeyRotateParams) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} diff --git a/apikey_test.go b/apikey_test.go index 6d19cf5..8ff53ed 100644 --- a/apikey_test.go +++ b/apikey_test.go @@ -150,3 +150,33 @@ func TestAPIKeyDelete(t *testing.T) { t.Fatalf("err should be nil: %s", err.Error()) } } + +func TestAPIKeyRotateWithOptionalParams(t *testing.T) { + t.Skip("Mock server 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.APIKeys.Rotate( + context.TODO(), + "id", + kernel.APIKeyRotateParams{ + DaysToExpire: kernel.Int(30), + ExpireInDays: kernel.Int(7), + }, + ) + 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()) + } +} From 9e2350674aafa75a45c4b843d8a43b31c4656bd6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 14 Jun 2026 20:21:14 +0000 Subject: [PATCH 4/6] refactor(api): align API key audit surface with browser sibling (KERNEL-1350) --- .stats.yml | 4 ++-- apikey.go | 39 +++++++++++++++++++-------------------- apikey_test.go | 1 + 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.stats.yml b/.stats.yml index 8d8fe88..4127b5f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 120 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-1f10598afa01d76d22b0ed63685248f482f74c9353cffe1d3e4a3d38da7716cf.yml -openapi_spec_hash: 8936f458bfa681b709e459ca1cc76fb5 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-e8afdbeac9332cf79200c2eb873e532104fd0a7472b08e63cde6c857a87cf0c3.yml +openapi_spec_hash: 2525caf30dffbdd83c83948201f11a52 config_hash: 03c7e57f268c750e2415831662e95969 diff --git a/apikey.go b/apikey.go index 0b08caa..b7b4132 100644 --- a/apikey.go +++ b/apikey.go @@ -144,12 +144,6 @@ type APIKey struct { // Project name for project-scoped API keys. Null means the key is org-wide or the // project name is unavailable. ProjectName string `json:"project_name" api:"required"` - // Derived lifecycle status of the API key. `active` means usable. `expired` means - // past its expires_at. `deleted` means it was deleted (soft-deleted) and can no - // longer authenticate. Deleted takes precedence over expired. - // - // Any of "active", "expired", "deleted". - Status APIKeyStatus `json:"status" api:"required"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { ID respjson.Field @@ -161,7 +155,6 @@ type APIKey struct { Name respjson.Field ProjectID respjson.Field ProjectName respjson.Field - Status respjson.Field ExtraFields map[string]respjson.Field raw string } `json:"-"` @@ -196,17 +189,6 @@ func (r *APIKeyCreatedBy) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } -// Derived lifecycle status of the API key. `active` means usable. `expired` means -// past its expires_at. `deleted` means it was deleted (soft-deleted) and can no -// longer authenticate. Deleted takes precedence over expired. -type APIKeyStatus string - -const ( - APIKeyStatusActive APIKeyStatus = "active" - APIKeyStatusExpired APIKeyStatus = "expired" - APIKeyStatusDeleted APIKeyStatus = "deleted" -) - // API key returned immediately after creation. Includes the plaintext key once. type CreatedAPIKey struct { // Plaintext API key. Only returned once when the key is created. @@ -274,8 +256,8 @@ func (r *APIKeyUpdateParams) UnmarshalJSON(data []byte) error { } type APIKeyListParams struct { - // When true, include deleted (soft-deleted) API keys in the results for audit - // purposes. Defaults to false, which returns only live keys. + // Deprecated: use status=all instead. When true, include deleted (soft-deleted) + // API keys in the results for audit purposes. IncludeDeleted param.Opt[bool] `query:"include_deleted,omitzero" json:"-"` // Maximum number of results to return Limit param.Opt[int64] `query:"limit,omitzero" json:"-"` @@ -292,6 +274,12 @@ type APIKeyListParams struct { // // Any of "asc", "desc". SortDirection APIKeyListParamsSortDirection `query:"sort_direction,omitzero" json:"-"` + // Filter API keys by status. "active" returns keys that are not deleted (default; + // expired-but-not-deleted keys are still included), "deleted" returns only + // soft-deleted keys, "all" returns both. + // + // Any of "active", "deleted", "all". + Status APIKeyListParamsStatus `query:"status,omitzero" json:"-"` paramObj } @@ -320,6 +308,17 @@ const ( APIKeyListParamsSortDirectionDesc APIKeyListParamsSortDirection = "desc" ) +// Filter API keys by status. "active" returns keys that are not deleted (default; +// expired-but-not-deleted keys are still included), "deleted" returns only +// soft-deleted keys, "all" returns both. +type APIKeyListParamsStatus string + +const ( + APIKeyListParamsStatusActive APIKeyListParamsStatus = "active" + APIKeyListParamsStatusDeleted APIKeyListParamsStatus = "deleted" + APIKeyListParamsStatusAll APIKeyListParamsStatus = "all" +) + type APIKeyRotateParams struct { // Lifetime in days for the new key, up to 3650. Omit to reuse the rotated key's // original lifetime, or never-expires if it had none. diff --git a/apikey_test.go b/apikey_test.go index 8ff53ed..24d41c6 100644 --- a/apikey_test.go +++ b/apikey_test.go @@ -118,6 +118,7 @@ func TestAPIKeyListWithOptionalParams(t *testing.T) { Query: kernel.String("query"), SortBy: kernel.APIKeyListParamsSortByCreatedAt, SortDirection: kernel.APIKeyListParamsSortDirectionAsc, + Status: kernel.APIKeyListParamsStatusActive, }) if err != nil { var apierr *kernel.Error From f81f420a4bfad5fe1519f1e768fc06fb1cb77712 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 18:41:29 +0000 Subject: [PATCH 5/6] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 4127b5f..2919208 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 120 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-e8afdbeac9332cf79200c2eb873e532104fd0a7472b08e63cde6c857a87cf0c3.yml -openapi_spec_hash: 2525caf30dffbdd83c83948201f11a52 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-d26459bd3514237e8d757be3cbdc76ca62f6083504b85601e57db830888964f7.yml +openapi_spec_hash: 5dd151a8099398819a97692c1c60c3c6 config_hash: 03c7e57f268c750e2415831662e95969 From 8a5b3b62cf9ad533bf1a67cfe9411cca60d57a37 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 18:41:57 +0000 Subject: [PATCH 6/6] release: 0.68.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 14 ++++++++++++++ README.md | 2 +- internal/version.go | 2 +- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index db00cae..0b8a7c6 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.67.0" + ".": "0.68.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a9a1c6e..fd9d3ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 0.68.0 (2026-06-15) + +Full Changelog: [v0.67.0...v0.68.0](https://github.com/kernel/kernel-go-sdk/compare/v0.67.0...v0.68.0) + +### Features + +* Add API key rotate endpoint ([04d4a12](https://github.com/kernel/kernel-go-sdk/commit/04d4a12f1f98a5745b7ca706a22cab71cb625866)) +* **api:** surface deleted/expired API keys for audit trail (KERNEL-1350) ([e7f9fd8](https://github.com/kernel/kernel-go-sdk/commit/e7f9fd8aa890b660d675b22ad48f869504817b2d)) + + +### Refactors + +* **api:** align API key audit surface with browser sibling (KERNEL-1350) ([9e23506](https://github.com/kernel/kernel-go-sdk/commit/9e2350674aafa75a45c4b843d8a43b31c4656bd6)) + ## 0.67.0 (2026-06-11) Full Changelog: [v0.66.0...v0.67.0](https://github.com/kernel/kernel-go-sdk/compare/v0.66.0...v0.67.0) diff --git a/README.md b/README.md index 737e769..5ddd1ff 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.67.0' +go get -u 'github.com/kernel/kernel-go-sdk@v0.68.0' ``` diff --git a/internal/version.go b/internal/version.go index 1137ca0..15b7357 100644 --- a/internal/version.go +++ b/internal/version.go @@ -2,4 +2,4 @@ package internal -const PackageVersion = "0.67.0" // x-release-please-version +const PackageVersion = "0.68.0" // x-release-please-version