From 68b465ad5ccadfc8fe41242f415c37755438ff22 Mon Sep 17 00:00:00 2001 From: Fayzan Ahmed Date: Wed, 9 Sep 2026 13:28:15 +0000 Subject: [PATCH] Display expires_at on API keys The manage API now returns expires_at on the key list and detail responses. Show it as an EXPIRES column in 'api-keys list' and an Expires field in 'api-keys show', rendering a missing value as "never" to match 'set expiry never'. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- cmd/api_keys.go | 4 +++- cmd/output.go | 10 ++++++++++ internal/client/api_keys.go | 4 +++- internal/client/api_keys_test.go | 13 +++++++++++++ 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/cmd/api_keys.go b/cmd/api_keys.go index cc3004e..e71e36f 100644 --- a/cmd/api_keys.go +++ b/cmd/api_keys.go @@ -74,11 +74,12 @@ func newAPIKeysListCommand(env environment) *cobra.Command { key.Name, formatMoney(key.MonthlySpend), formatLimit(key.MonthlyLimit), + formatExpiry(key.ExpiresAt), formatLabels(key.Labels), }) } - return writeTable(out, []string{"ID", "NAME", "SPEND", "LIMIT", "LABELS"}, rows) + return writeTable(out, []string{"ID", "NAME", "SPEND", "LIMIT", "EXPIRES", "LABELS"}, rows) }, } } @@ -112,6 +113,7 @@ func newAPIKeysShowCommand(env environment) *cobra.Command { {"Name", key.Name}, {"Spend this month", formatMoney(key.MonthlySpend)}, {"Monthly limit", formatLimit(key.MonthlyLimit)}, + {"Expires", formatExpiry(key.ExpiresAt)}, {"Permissions", formatPermissions(key.Permissions)}, {"Logging", strconv.FormatBool(key.Logging)}, {"Group", formatGroup(key.Group)}, diff --git a/cmd/output.go b/cmd/output.go index 1638024..594835e 100644 --- a/cmd/output.go +++ b/cmd/output.go @@ -11,6 +11,7 @@ import ( "time" "github.com/requestyai/cli/internal/client" + "github.com/requestyai/cli/internal/util" "github.com/shopspring/decimal" "github.com/spf13/cobra" ) @@ -102,6 +103,15 @@ func formatTime(moment time.Time) string { return moment.Format(time.RFC3339) } +// formatExpiry renders when a key stops working, or that it never does. +func formatExpiry(moment *time.Time) string { + if moment == nil { + return util.NeverExpires + } + + return formatTime(*moment) +} + // formatDate renders a timestamp as a calendar date, to keep table rows narrow. func formatDate(moment time.Time) string { if moment.IsZero() { diff --git a/internal/client/api_keys.go b/internal/client/api_keys.go index 246e48f..89c6d2e 100644 --- a/internal/client/api_keys.go +++ b/internal/client/api_keys.go @@ -39,12 +39,13 @@ type APIKeyGroup struct { } // APIKey is a key as it appears in the organization listing. A MonthlyLimit of -// zero means the key is not capped. +// zero means the key is not capped, and a nil ExpiresAt means it never expires. type APIKey struct { ID string `json:"id"` Name string `json:"name"` MonthlyLimit decimal.Decimal `json:"monthly_limit"` MonthlySpend decimal.Decimal `json:"monthly_spend"` + ExpiresAt *time.Time `json:"expires_at,omitempty"` Permissions APIKeyPermissions `json:"permissions"` Labels map[string]string `json:"labels,omitempty"` CreatedBy *APIKeyUser `json:"created_by,omitempty"` @@ -59,6 +60,7 @@ type APIKeyDetails struct { Logging bool `json:"logging"` MonthlyLimit decimal.Decimal `json:"monthly_limit"` MonthlySpend decimal.Decimal `json:"monthly_spend"` + ExpiresAt *time.Time `json:"expires_at,omitempty"` Permissions APIKeyPermissions `json:"permissions"` Group *APIKeyGroup `json:"group,omitempty"` } diff --git a/internal/client/api_keys_test.go b/internal/client/api_keys_test.go index 148342c..0e6e7d2 100644 --- a/internal/client/api_keys_test.go +++ b/internal/client/api_keys_test.go @@ -51,8 +51,18 @@ func newTestClient(t *testing.T, status int, reply string) (*Client, *recorder) return New(config.Config{APIBaseURL: server.URL, APIKey: "test-key"}), seen } +func mustParseTime(t *testing.T, value string) *time.Time { + t.Helper() + + parsed, err := time.Parse(time.RFC3339, value) + require.NoError(t, err) + + return &parsed +} + func TestClientAPIKeys(t *testing.T) { reply := `{"keys":[{"id":"key-1","name":"production","monthly_limit":"500","monthly_spend":"12.5",` + + `"expires_at":"2026-12-31T23:59:59Z",` + `"permissions":{"manage":"read","completions":"write"},"labels":{"env":"prod"},` + `"created_by":{"id":"user-1","email":"you@example.com"},"group":{"id":"group-1"}}]}` client, seen := newTestClient(t, http.StatusOK, reply) @@ -68,6 +78,7 @@ func TestClientAPIKeys(t *testing.T) { Name: "production", MonthlyLimit: decimal.RequireFromString("500"), MonthlySpend: decimal.RequireFromString("12.5"), + ExpiresAt: mustParseTime(t, "2026-12-31T23:59:59Z"), Permissions: APIKeyPermissions{Manage: APIKeyPermissionRead, Completions: APIKeyPermissionWrite}, Labels: map[string]string{"env": "prod"}, CreatedBy: &APIKeyUser{ID: "user-1", Email: "you@example.com"}, @@ -77,6 +88,7 @@ func TestClientAPIKeys(t *testing.T) { func TestClientAPIKey(t *testing.T) { reply := `{"id":"key-1","name":"production","logging":true,"monthly_limit":"0","monthly_spend":"3",` + + `"expires_at":"2026-12-31T23:59:59Z",` + `"permissions":{"manage":"none","completions":"write"},"group":{"id":"group-1"}}` client, seen := newTestClient(t, http.StatusOK, reply) @@ -91,6 +103,7 @@ func TestClientAPIKey(t *testing.T) { Logging: true, MonthlyLimit: decimal.RequireFromString("0"), MonthlySpend: decimal.RequireFromString("3"), + ExpiresAt: mustParseTime(t, "2026-12-31T23:59:59Z"), Permissions: APIKeyPermissions{Manage: APIKeyPermissionNone, Completions: APIKeyPermissionWrite}, Group: &APIKeyGroup{ID: "group-1"}, }, key)