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/.stats.yml b/.stats.yml index a164009..2919208 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 -config_hash: 57567e00b41af47cef1b78e51b747aa0 +configured_endpoints: 120 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/kernel-d26459bd3514237e8d757be3cbdc76ca62f6083504b85601e57db830888964f7.yml +openapi_spec_hash: 5dd151a8099398819a97692c1c60c3c6 +config_hash: 03c7e57f268c750e2415831662e95969 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/api.md b/api.md index f4a6c67..0c51245 100644 --- a/api.md +++ b/api.md @@ -427,10 +427,11 @@ 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 +- client.APIKeys.Rotate(ctx context.Context, id string, body kernel.APIKeyRotateParams) (\*kernel.CreatedAPIKey, error) # CredentialProviders diff --git a/apikey.go b/apikey.go index 1fed55a..b7b4132 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 } @@ -110,12 +110,29 @@ 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"` // 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 @@ -132,6 +149,7 @@ type APIKey struct { ID respjson.Field CreatedAt respjson.Field CreatedBy respjson.Field + DeletedAt respjson.Field ExpiresAt respjson.Field MaskedKey respjson.Field Name respjson.Field @@ -208,6 +226,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 +256,9 @@ func (r *APIKeyUpdateParams) UnmarshalJSON(data []byte) error { } type APIKeyListParams struct { + // 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:"-"` // Number of results to skip @@ -238,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 } @@ -265,3 +307,32 @@ const ( APIKeyListParamsSortDirectionAsc APIKeyListParamsSortDirection = "asc" 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. + 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 946b46b..24d41c6 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,13 @@ 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, + Status: kernel.APIKeyListParamsStatusActive, }) if err != nil { var apierr *kernel.Error @@ -143,3 +151,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()) + } +} 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