From 006329c7ad628abc20c95f7e4895568e35ec641c Mon Sep 17 00:00:00 2001 From: chronark Date: Fri, 18 Jul 2025 18:42:35 +0200 Subject: [PATCH 1/9] fix: scope cache keys to a workspace to prevent leaking of data --- go/apps/api/routes/register.go | 42 +++++----- .../v2_ratelimit_delete_override/200_test.go | 10 +-- .../v2_ratelimit_delete_override/400_test.go | 8 +- .../v2_ratelimit_delete_override/401_test.go | 10 +-- .../v2_ratelimit_delete_override/403_test.go | 10 +-- .../v2_ratelimit_delete_override/404_test.go | 10 +-- .../v2_ratelimit_delete_override/handler.go | 21 +++-- .../v2_ratelimit_get_override/200_test.go | 8 +- .../v2_ratelimit_get_override/400_test.go | 8 +- .../v2_ratelimit_get_override/401_test.go | 8 +- .../v2_ratelimit_get_override/403_test.go | 8 +- .../v2_ratelimit_get_override/404_test.go | 8 +- .../v2_ratelimit_get_override/handler.go | 8 +- .../api/routes/v2_ratelimit_limit/200_test.go | 12 +-- .../api/routes/v2_ratelimit_limit/400_test.go | 10 +-- .../api/routes/v2_ratelimit_limit/401_test.go | 10 +-- .../api/routes/v2_ratelimit_limit/403_test.go | 10 +-- .../api/routes/v2_ratelimit_limit/404_test.go | 10 +-- .../v2_ratelimit_limit/accuracy_test.go | 12 +-- .../api/routes/v2_ratelimit_limit/handler.go | 80 ++++++++++--------- .../v2_ratelimit_set_override/200_test.go | 10 +-- .../v2_ratelimit_set_override/400_test.go | 8 +- .../v2_ratelimit_set_override/401_test.go | 10 +-- .../v2_ratelimit_set_override/403_test.go | 10 +-- .../v2_ratelimit_set_override/404_test.go | 10 +-- .../v2_ratelimit_set_override/handler.go | 21 +++-- go/internal/services/caches/caches.go | 14 ++-- go/pkg/cache/cache.go | 8 +- go/pkg/cache/interface.go | 5 +- go/pkg/cache/middleware/tracing.go | 10 ++- go/pkg/cache/noop.go | 2 +- go/pkg/cache/scoped_key.go | 57 +++++++++++++ 32 files changed, 274 insertions(+), 194 deletions(-) create mode 100644 go/pkg/cache/scoped_key.go diff --git a/go/apps/api/routes/register.go b/go/apps/api/routes/register.go index cdacb5f58c..34bd56a2e4 100644 --- a/go/apps/api/routes/register.go +++ b/go/apps/api/routes/register.go @@ -73,13 +73,13 @@ func Register(srv *zen.Server, svc *Services) { srv.RegisterRoute( defaultMiddlewares, &v2RatelimitLimit.Handler{ - Logger: svc.Logger, - DB: svc.Database, - Keys: svc.Keys, - ClickHouse: svc.ClickHouse, - Ratelimit: svc.Ratelimit, - RatelimitNamespaceByNameCache: svc.Caches.RatelimitNamespaceByName, - TestMode: srv.Flags().TestMode, + Logger: svc.Logger, + DB: svc.Database, + Keys: svc.Keys, + ClickHouse: svc.ClickHouse, + Ratelimit: svc.Ratelimit, + RatelimitNamespaceCache: svc.Caches.RatelimitNamespace, + TestMode: srv.Flags().TestMode, }, ) @@ -87,11 +87,11 @@ func Register(srv *zen.Server, svc *Services) { srv.RegisterRoute( defaultMiddlewares, &v2RatelimitSetOverride.Handler{ - Logger: svc.Logger, - DB: svc.Database, - Keys: svc.Keys, - Auditlogs: svc.Auditlogs, - RatelimitNamespaceByNameCache: svc.Caches.RatelimitNamespaceByName, + Logger: svc.Logger, + DB: svc.Database, + Keys: svc.Keys, + Auditlogs: svc.Auditlogs, + RatelimitNamespaceCache: svc.Caches.RatelimitNamespace, }, ) @@ -99,10 +99,10 @@ func Register(srv *zen.Server, svc *Services) { srv.RegisterRoute( defaultMiddlewares, &v2RatelimitGetOverride.Handler{ - Logger: svc.Logger, - DB: svc.Database, - Keys: svc.Keys, - RatelimitNamespaceByNameCache: svc.Caches.RatelimitNamespaceByName, + Logger: svc.Logger, + DB: svc.Database, + Keys: svc.Keys, + RatelimitNamespaceCache: svc.Caches.RatelimitNamespace, }, ) @@ -110,11 +110,11 @@ func Register(srv *zen.Server, svc *Services) { srv.RegisterRoute( defaultMiddlewares, &v2RatelimitDeleteOverride.Handler{ - Logger: svc.Logger, - DB: svc.Database, - Keys: svc.Keys, - Auditlogs: svc.Auditlogs, - RatelimitNamespaceByNameCache: svc.Caches.RatelimitNamespaceByName, + Logger: svc.Logger, + DB: svc.Database, + Keys: svc.Keys, + Auditlogs: svc.Auditlogs, + RatelimitNamespaceCache: svc.Caches.RatelimitNamespace, }, ) diff --git a/go/apps/api/routes/v2_ratelimit_delete_override/200_test.go b/go/apps/api/routes/v2_ratelimit_delete_override/200_test.go index da40cea0f5..a6358e9806 100644 --- a/go/apps/api/routes/v2_ratelimit_delete_override/200_test.go +++ b/go/apps/api/routes/v2_ratelimit_delete_override/200_test.go @@ -44,11 +44,11 @@ func TestDeleteOverrideSuccessfully(t *testing.T) { require.NoError(t, err) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_delete_override/400_test.go b/go/apps/api/routes/v2_ratelimit_delete_override/400_test.go index 163d590ef3..8587200d45 100644 --- a/go/apps/api/routes/v2_ratelimit_delete_override/400_test.go +++ b/go/apps/api/routes/v2_ratelimit_delete_override/400_test.go @@ -18,10 +18,10 @@ func TestBadRequests(t *testing.T) { rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_delete_override/401_test.go b/go/apps/api/routes/v2_ratelimit_delete_override/401_test.go index 31b74d6c1a..c34f054203 100644 --- a/go/apps/api/routes/v2_ratelimit_delete_override/401_test.go +++ b/go/apps/api/routes/v2_ratelimit_delete_override/401_test.go @@ -14,11 +14,11 @@ func TestUnauthorizedAccess(t *testing.T) { h := testutil.NewHarness(t) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_delete_override/403_test.go b/go/apps/api/routes/v2_ratelimit_delete_override/403_test.go index d3b2a56256..4e0ab744b6 100644 --- a/go/apps/api/routes/v2_ratelimit_delete_override/403_test.go +++ b/go/apps/api/routes/v2_ratelimit_delete_override/403_test.go @@ -45,11 +45,11 @@ func TestWorkspacePermissions(t *testing.T) { require.NoError(t, err) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_delete_override/404_test.go b/go/apps/api/routes/v2_ratelimit_delete_override/404_test.go index 396614c050..efe7a1d5ff 100644 --- a/go/apps/api/routes/v2_ratelimit_delete_override/404_test.go +++ b/go/apps/api/routes/v2_ratelimit_delete_override/404_test.go @@ -31,11 +31,11 @@ func TestNotFound(t *testing.T) { require.NoError(t, err) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_delete_override/handler.go b/go/apps/api/routes/v2_ratelimit_delete_override/handler.go index 4384b3e7f9..dce3de6c48 100644 --- a/go/apps/api/routes/v2_ratelimit_delete_override/handler.go +++ b/go/apps/api/routes/v2_ratelimit_delete_override/handler.go @@ -25,11 +25,11 @@ type Response = openapi.V2RatelimitDeleteOverrideResponseBody // Handler implements zen.Route interface for the v2 ratelimit delete override endpoint type Handler struct { - Logger logging.Logger - DB db.Database - Keys keys.KeyService - Auditlogs auditlogs.AuditLogService - RatelimitNamespaceByNameCache cache.Cache[string, db.FindRatelimitNamespace] + Logger logging.Logger + DB db.Database + Keys keys.KeyService + Auditlogs auditlogs.AuditLogService + RatelimitNamespaceCache cache.Cache[cache.ScopedKey, db.FindRatelimitNamespace] } // Method returns the HTTP method this route responds to @@ -156,7 +156,16 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { return err } - h.RatelimitNamespaceByNameCache.Remove(ctx, namespace.Name) + h.RatelimitNamespaceCache.Remove(ctx, + cache.ScopedKey{ + WorkspaceID: auth.AuthorizedWorkspaceID, + Key: namespace.ID, + }, + cache.ScopedKey{ + WorkspaceID: auth.AuthorizedWorkspaceID, + Key: namespace.Name, + }, + ) return nil }) diff --git a/go/apps/api/routes/v2_ratelimit_get_override/200_test.go b/go/apps/api/routes/v2_ratelimit_get_override/200_test.go index 8e884e62dd..12dec85e4e 100644 --- a/go/apps/api/routes/v2_ratelimit_get_override/200_test.go +++ b/go/apps/api/routes/v2_ratelimit_get_override/200_test.go @@ -47,10 +47,10 @@ func TestGetOverrideSuccessfully(t *testing.T) { require.NoError(t, err) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_get_override/400_test.go b/go/apps/api/routes/v2_ratelimit_get_override/400_test.go index f813c4fbf4..f0c3d25d97 100644 --- a/go/apps/api/routes/v2_ratelimit_get_override/400_test.go +++ b/go/apps/api/routes/v2_ratelimit_get_override/400_test.go @@ -19,10 +19,10 @@ func TestBadRequests(t *testing.T) { rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_get_override/401_test.go b/go/apps/api/routes/v2_ratelimit_get_override/401_test.go index 59d304c7b7..e4f3b62abf 100644 --- a/go/apps/api/routes/v2_ratelimit_get_override/401_test.go +++ b/go/apps/api/routes/v2_ratelimit_get_override/401_test.go @@ -14,10 +14,10 @@ func TestUnauthorizedAccess(t *testing.T) { h := testutil.NewHarness(t) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_get_override/403_test.go b/go/apps/api/routes/v2_ratelimit_get_override/403_test.go index aa60db1006..c076d5bc28 100644 --- a/go/apps/api/routes/v2_ratelimit_get_override/403_test.go +++ b/go/apps/api/routes/v2_ratelimit_get_override/403_test.go @@ -45,10 +45,10 @@ func TestWorkspacePermissions(t *testing.T) { require.NoError(t, err) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_get_override/404_test.go b/go/apps/api/routes/v2_ratelimit_get_override/404_test.go index ac7f1a9fae..b47f697a98 100644 --- a/go/apps/api/routes/v2_ratelimit_get_override/404_test.go +++ b/go/apps/api/routes/v2_ratelimit_get_override/404_test.go @@ -31,10 +31,10 @@ func TestOverrideNotFound(t *testing.T) { require.NoError(t, err) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_get_override/handler.go b/go/apps/api/routes/v2_ratelimit_get_override/handler.go index dd5362c8cf..b4ae042245 100644 --- a/go/apps/api/routes/v2_ratelimit_get_override/handler.go +++ b/go/apps/api/routes/v2_ratelimit_get_override/handler.go @@ -26,10 +26,10 @@ type Response = openapi.V2RatelimitGetOverrideResponseBody // Handler implements zen.Route interface for the v2 ratelimit get override endpoint type Handler struct { // Services as public fields - Logger logging.Logger - DB db.Database - Keys keys.KeyService - RatelimitNamespaceByNameCache cache.Cache[string, db.FindRatelimitNamespace] + Logger logging.Logger + DB db.Database + Keys keys.KeyService + RatelimitNamespaceCache cache.Cache[cache.ScopedKey, db.FindRatelimitNamespace] } // Method returns the HTTP method this route responds to diff --git a/go/apps/api/routes/v2_ratelimit_limit/200_test.go b/go/apps/api/routes/v2_ratelimit_limit/200_test.go index 6d0113dd61..4e14cb9c9a 100644 --- a/go/apps/api/routes/v2_ratelimit_limit/200_test.go +++ b/go/apps/api/routes/v2_ratelimit_limit/200_test.go @@ -22,12 +22,12 @@ func TestLimitSuccessfully(t *testing.T) { h := testutil.NewHarness(t) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - ClickHouse: h.ClickHouse, - Ratelimit: h.Ratelimit, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + ClickHouse: h.ClickHouse, + Ratelimit: h.Ratelimit, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_limit/400_test.go b/go/apps/api/routes/v2_ratelimit_limit/400_test.go index a77099c626..0817b08a98 100644 --- a/go/apps/api/routes/v2_ratelimit_limit/400_test.go +++ b/go/apps/api/routes/v2_ratelimit_limit/400_test.go @@ -20,11 +20,11 @@ func TestBadRequests(t *testing.T) { h := testutil.NewHarness(t) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Ratelimit: h.Ratelimit, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Ratelimit: h.Ratelimit, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_limit/401_test.go b/go/apps/api/routes/v2_ratelimit_limit/401_test.go index 239898523f..e50ea8ac25 100644 --- a/go/apps/api/routes/v2_ratelimit_limit/401_test.go +++ b/go/apps/api/routes/v2_ratelimit_limit/401_test.go @@ -13,11 +13,11 @@ func TestUnauthorizedAccess(t *testing.T) { h := testutil.NewHarness(t) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Ratelimit: h.Ratelimit, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Ratelimit: h.Ratelimit, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_limit/403_test.go b/go/apps/api/routes/v2_ratelimit_limit/403_test.go index 176ad9fa93..5386b683ae 100644 --- a/go/apps/api/routes/v2_ratelimit_limit/403_test.go +++ b/go/apps/api/routes/v2_ratelimit_limit/403_test.go @@ -31,11 +31,11 @@ func TestWorkspacePermissions(t *testing.T) { require.NoError(t, err) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Ratelimit: h.Ratelimit, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Ratelimit: h.Ratelimit, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_limit/404_test.go b/go/apps/api/routes/v2_ratelimit_limit/404_test.go index 5b91d0c61f..a2f7a5446b 100644 --- a/go/apps/api/routes/v2_ratelimit_limit/404_test.go +++ b/go/apps/api/routes/v2_ratelimit_limit/404_test.go @@ -20,11 +20,11 @@ func TestNamespaceNotFound(t *testing.T) { h := testutil.NewHarness(t) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Ratelimit: h.Ratelimit, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Ratelimit: h.Ratelimit, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_limit/accuracy_test.go b/go/apps/api/routes/v2_ratelimit_limit/accuracy_test.go index 44255c9fb9..d00c456eb5 100644 --- a/go/apps/api/routes/v2_ratelimit_limit/accuracy_test.go +++ b/go/apps/api/routes/v2_ratelimit_limit/accuracy_test.go @@ -57,12 +57,12 @@ func TestRateLimitAccuracy(t *testing.T) { h := testutil.NewHarness(t) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - ClickHouse: h.ClickHouse, - Ratelimit: h.Ratelimit, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + ClickHouse: h.ClickHouse, + Ratelimit: h.Ratelimit, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) ctx := context.Background() diff --git a/go/apps/api/routes/v2_ratelimit_limit/handler.go b/go/apps/api/routes/v2_ratelimit_limit/handler.go index e042d1f00c..03b1471787 100644 --- a/go/apps/api/routes/v2_ratelimit_limit/handler.go +++ b/go/apps/api/routes/v2_ratelimit_limit/handler.go @@ -32,13 +32,13 @@ type Response = openapi.V2RatelimitLimitResponseBody // Handler implements zen.Route interface for the v2 ratelimit limit endpoint type Handler struct { // Services as public fields - Logger logging.Logger - Keys keys.KeyService - DB db.Database - ClickHouse clickhouse.Bufferer - Ratelimit ratelimit.Service - RatelimitNamespaceByNameCache cache.Cache[string, db.FindRatelimitNamespace] - TestMode bool + Logger logging.Logger + Keys keys.KeyService + DB db.Database + ClickHouse clickhouse.Bufferer + Ratelimit ratelimit.Service + RatelimitNamespaceCache cache.Cache[cache.ScopedKey, db.FindRatelimitNamespace] + TestMode bool } // Method returns the HTTP method this route responds to @@ -70,43 +70,45 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { } ctx, span := tracing.Start(ctx, "FindRatelimitNamespace") - namespace, err := h.RatelimitNamespaceByNameCache.SWR(ctx, req.Namespace, func(ctx context.Context) (db.FindRatelimitNamespace, error) { - response, err := db.Query.FindRatelimitNamespace(ctx, h.DB.RO(), db.FindRatelimitNamespaceParams{ - WorkspaceID: auth.AuthorizedWorkspaceID, - Name: sql.NullString{String: req.Namespace, Valid: true}, - ID: sql.NullString{String: "", Valid: false}, - }) - result := db.FindRatelimitNamespace{} // nolint:exhaustruct - if err != nil { - return result, err - } + namespace, err := h.RatelimitNamespaceCache.SWR(ctx, + cache.ScopedKey{WorkspaceID: auth.AuthorizedWorkspaceID, Key: req.Namespace}, + func(ctx context.Context) (db.FindRatelimitNamespace, error) { + response, err := db.Query.FindRatelimitNamespace(ctx, h.DB.RO(), db.FindRatelimitNamespaceParams{ + WorkspaceID: auth.AuthorizedWorkspaceID, + Name: sql.NullString{String: req.Namespace, Valid: true}, + ID: sql.NullString{String: "", Valid: false}, + }) + result := db.FindRatelimitNamespace{} // nolint:exhaustruct + if err != nil { + return result, err + } - result = db.FindRatelimitNamespace{ - ID: response.ID, - WorkspaceID: response.WorkspaceID, - Name: response.Name, - CreatedAtM: response.CreatedAtM, - UpdatedAtM: response.UpdatedAtM, - DeletedAtM: response.DeletedAtM, - DirectOverrides: make(map[string]db.FindRatelimitNamespaceLimitOverride), - WildcardOverrides: make([]db.FindRatelimitNamespaceLimitOverride, 0), - } + result = db.FindRatelimitNamespace{ + ID: response.ID, + WorkspaceID: response.WorkspaceID, + Name: response.Name, + CreatedAtM: response.CreatedAtM, + UpdatedAtM: response.UpdatedAtM, + DeletedAtM: response.DeletedAtM, + DirectOverrides: make(map[string]db.FindRatelimitNamespaceLimitOverride), + WildcardOverrides: make([]db.FindRatelimitNamespaceLimitOverride, 0), + } - overrides := make([]db.FindRatelimitNamespaceLimitOverride, 0) - err = json.Unmarshal(response.Overrides.([]byte), &overrides) - if err != nil { - return result, err - } + overrides := make([]db.FindRatelimitNamespaceLimitOverride, 0) + err = json.Unmarshal(response.Overrides.([]byte), &overrides) + if err != nil { + return result, err + } - for _, override := range overrides { - result.DirectOverrides[override.Identifier] = override - if strings.Contains(override.Identifier, "*") { - result.WildcardOverrides = append(result.WildcardOverrides, override) + for _, override := range overrides { + result.DirectOverrides[override.Identifier] = override + if strings.Contains(override.Identifier, "*") { + result.WildcardOverrides = append(result.WildcardOverrides, override) + } } - } - return result, nil - }, caches.DefaultFindFirstOp) + return result, nil + }, caches.DefaultFindFirstOp) span.End() if err != nil { diff --git a/go/apps/api/routes/v2_ratelimit_set_override/200_test.go b/go/apps/api/routes/v2_ratelimit_set_override/200_test.go index 2b3553a657..7e5e671f9c 100644 --- a/go/apps/api/routes/v2_ratelimit_set_override/200_test.go +++ b/go/apps/api/routes/v2_ratelimit_set_override/200_test.go @@ -30,11 +30,11 @@ func TestSetOverrideSuccessfully(t *testing.T) { require.NoError(t, err) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_set_override/400_test.go b/go/apps/api/routes/v2_ratelimit_set_override/400_test.go index 30b6f13c8a..1023b3dc24 100644 --- a/go/apps/api/routes/v2_ratelimit_set_override/400_test.go +++ b/go/apps/api/routes/v2_ratelimit_set_override/400_test.go @@ -18,10 +18,10 @@ func TestBadRequests(t *testing.T) { rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "ratelimit.*.set_override") route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_set_override/401_test.go b/go/apps/api/routes/v2_ratelimit_set_override/401_test.go index 32109244eb..ab6e131c8c 100644 --- a/go/apps/api/routes/v2_ratelimit_set_override/401_test.go +++ b/go/apps/api/routes/v2_ratelimit_set_override/401_test.go @@ -14,11 +14,11 @@ func TestUnauthorizedAccess(t *testing.T) { h := testutil.NewHarness(t) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_set_override/403_test.go b/go/apps/api/routes/v2_ratelimit_set_override/403_test.go index 8018a6126a..9c02ccc4b3 100644 --- a/go/apps/api/routes/v2_ratelimit_set_override/403_test.go +++ b/go/apps/api/routes/v2_ratelimit_set_override/403_test.go @@ -31,11 +31,11 @@ func TestWorkspacePermissions(t *testing.T) { require.NoError(t, err) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_set_override/404_test.go b/go/apps/api/routes/v2_ratelimit_set_override/404_test.go index 3eac1a6715..2458fe82d0 100644 --- a/go/apps/api/routes/v2_ratelimit_set_override/404_test.go +++ b/go/apps/api/routes/v2_ratelimit_set_override/404_test.go @@ -15,11 +15,11 @@ func TestNamespaceNotFound(t *testing.T) { h := testutil.NewHarness(t) route := &handler.Handler{ - DB: h.DB, - Keys: h.Keys, - Logger: h.Logger, - Auditlogs: h.Auditlogs, - RatelimitNamespaceByNameCache: h.Caches.RatelimitNamespaceByName, + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Auditlogs: h.Auditlogs, + RatelimitNamespaceCache: h.Caches.RatelimitNamespace, } h.Register(route) diff --git a/go/apps/api/routes/v2_ratelimit_set_override/handler.go b/go/apps/api/routes/v2_ratelimit_set_override/handler.go index 860c696b1c..e8185a4cf0 100644 --- a/go/apps/api/routes/v2_ratelimit_set_override/handler.go +++ b/go/apps/api/routes/v2_ratelimit_set_override/handler.go @@ -28,11 +28,11 @@ type Response = openapi.V2RatelimitSetOverrideResponseBody // Handler implements zen.Route interface for the v2 ratelimit set override endpoint type Handler struct { // Services as public fields - Logger logging.Logger - DB db.Database - Keys keys.KeyService - Auditlogs auditlogs.AuditLogService - RatelimitNamespaceByNameCache cache.Cache[string, db.FindRatelimitNamespace] + Logger logging.Logger + DB db.Database + Keys keys.KeyService + Auditlogs auditlogs.AuditLogService + RatelimitNamespaceCache cache.Cache[cache.ScopedKey, db.FindRatelimitNamespace] } // Method returns the HTTP method this route responds to @@ -137,7 +137,16 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { return "", err } - h.RatelimitNamespaceByNameCache.Remove(ctx, namespace.Name) + h.RatelimitNamespaceCache.Remove(ctx, + cache.ScopedKey{ + WorkspaceID: auth.AuthorizedWorkspaceID, + Key: namespace.ID, + }, + cache.ScopedKey{ + WorkspaceID: auth.AuthorizedWorkspaceID, + Key: namespace.Name, + }, + ) return overrideID, nil }) diff --git a/go/internal/services/caches/caches.go b/go/internal/services/caches/caches.go index eb2c96447d..1930fad62b 100644 --- a/go/internal/services/caches/caches.go +++ b/go/internal/services/caches/caches.go @@ -13,9 +13,9 @@ import ( // Caches holds all cache instances used throughout the application. // Each field represents a specialized cache for a specific data entity. type Caches struct { - // RatelimitNamespaceByName caches ratelimit namespace lookups by name. - // Keys are db.FindRatelimitNamespaceByNameParams and values are db.RatelimitNamespace. - RatelimitNamespaceByName cache.Cache[string, db.FindRatelimitNamespace] + // RatelimitNamespace caches ratelimit namespace lookups by name or ID. + // Keys are cache.ScopedKey and values are db.FindRatelimitNamespace. + RatelimitNamespace cache.Cache[cache.ScopedKey, db.FindRatelimitNamespace] // VerificationKeyByHash caches verification key lookups by their hash. // Keys are string (hash) and values are db.VerificationKey. @@ -65,7 +65,7 @@ type Config struct { // // Use the caches // key, err := caches.KeyByHash.Get(ctx, "some-hash") func New(config Config) (Caches, error) { - ratelimitNamespace, err := cache.New(cache.Config[string, db.FindRatelimitNamespace]{ + ratelimitNamespace, err := cache.New(cache.Config[cache.ScopedKey, db.FindRatelimitNamespace]{ Fresh: time.Minute, Stale: 24 * time.Hour, Logger: config.Logger, @@ -103,8 +103,8 @@ func New(config Config) (Caches, error) { } return Caches{ - RatelimitNamespaceByName: middleware.WithTracing(ratelimitNamespace), - ApiByID: middleware.WithTracing(apiById), - VerificationKeyByHash: middleware.WithTracing(verificationKeyByHash), + RatelimitNamespace: middleware.WithTracing(ratelimitNamespace), + ApiByID: middleware.WithTracing(apiById), + VerificationKeyByHash: middleware.WithTracing(verificationKeyByHash), }, nil } diff --git a/go/pkg/cache/cache.go b/go/pkg/cache/cache.go index 013fa30ea2..572edf1102 100644 --- a/go/pkg/cache/cache.go +++ b/go/pkg/cache/cache.go @@ -153,10 +153,10 @@ func (c *cache[K, V]) get(_ context.Context, key K) (swrEntry[V], bool) { return v, ok } -func (c *cache[K, V]) Remove(ctx context.Context, key K) { - - c.otter.Delete(key) - +func (c *cache[K, V]) Remove(ctx context.Context, keys ...K) { + for _, key := range keys { + c.otter.Delete(key) + } } func (c *cache[K, V]) Dump(ctx context.Context) ([]byte, error) { diff --git a/go/pkg/cache/interface.go b/go/pkg/cache/interface.go index 0978698e06..b8272bf206 100644 --- a/go/pkg/cache/interface.go +++ b/go/pkg/cache/interface.go @@ -15,8 +15,9 @@ type Cache[K comparable, V any] interface { // Sets the given key to null, indicating that the value does not exist in the origin. SetNull(ctx context.Context, key K) - // Removes the key from the cache. - Remove(ctx context.Context, key K) + // Remove removes one or more keys from the cache. + // Multiple keys can be provided for efficient bulk removal. + Remove(ctx context.Context, keys ...K) SWR(ctx context.Context, key K, refreshFromOrigin func(ctx context.Context) (V, error), op func(error) Op) (value V, err error) diff --git a/go/pkg/cache/middleware/tracing.go b/go/pkg/cache/middleware/tracing.go index e96360ea5d..27179e4d5f 100644 --- a/go/pkg/cache/middleware/tracing.go +++ b/go/pkg/cache/middleware/tracing.go @@ -44,13 +44,15 @@ func (mw *tracingMiddleware[K, V]) SetNull(ctx context.Context, key K) { mw.next.SetNull(ctx, key) } -func (mw *tracingMiddleware[K, V]) Remove(ctx context.Context, key K) { +func (mw *tracingMiddleware[K, V]) Remove(ctx context.Context, keys ...K) { ctx, span := tracing.Start(ctx, "cache.Remove") defer span.End() - span.SetAttributes(attribute.String("key", fmt.Sprintf("%+v", key))) - - mw.next.Remove(ctx, key) + span.SetAttributes( + attribute.String("keys", fmt.Sprintf("%+v", keys)), + attribute.Int("count", len(keys)), + ) + mw.next.Remove(ctx, keys...) } func (mw *tracingMiddleware[K, V]) Dump(ctx context.Context) ([]byte, error) { diff --git a/go/pkg/cache/noop.go b/go/pkg/cache/noop.go index e81fb6db2f..7e46d2a5e9 100644 --- a/go/pkg/cache/noop.go +++ b/go/pkg/cache/noop.go @@ -13,7 +13,7 @@ func (c *noopCache[K, V]) Get(ctx context.Context, key K) (value V, hit CacheHit func (c *noopCache[K, V]) Set(ctx context.Context, key K, value V) {} func (c *noopCache[K, V]) SetNull(ctx context.Context, key K) {} -func (c *noopCache[K, V]) Remove(ctx context.Context, key K) {} +func (c *noopCache[K, V]) Remove(ctx context.Context, keys ...K) {} func (c *noopCache[K, V]) Dump(ctx context.Context) ([]byte, error) { return []byte{}, nil diff --git a/go/pkg/cache/scoped_key.go b/go/pkg/cache/scoped_key.go new file mode 100644 index 0000000000..a560dd95dd --- /dev/null +++ b/go/pkg/cache/scoped_key.go @@ -0,0 +1,57 @@ +package cache + +// ScopedKey represents a cache key that is scoped to a specific workspace. +// +// This type is designed for caching data where keys are only unique within +// a workspace context, rather than being globally unique. For example, a user +// might create a ratelimit namespace called "api-calls" which is unique within +// their workspace but could exist in multiple workspaces. +// +// The ScopedKey ensures cache isolation between workspaces by combining the +// workspace ID with the resource key, preventing cache collisions and data +// leakage between different workspaces. +// +// # Usage +// +// Use ScopedKey when caching data that is workspace-specific: +// +// // Cache a ratelimit namespace by name +// key := cache.ScopedKey{ +// WorkspaceID: "ws_123", +// Key: "api-calls", +// } +// +// // Cache by ID (still workspace-scoped for consistency) +// key := cache.ScopedKey{ +// WorkspaceID: "ws_123", +// Key: "ns_456", +// } +// +// // Cache any workspace-scoped resource +// key := cache.ScopedKey{ +// WorkspaceID: "ws_123", +// Key: "some-resource-identifier", +// } +// +// # Design Rationale +// +// We chose this approach over concatenating strings because it provides type +// safety and makes the workspace scoping explicit in the API. It also allows +// for future extension if additional scoping dimensions are needed. +// +// The generic Key field can hold any string identifier (names, IDs, slugs, etc.) +// while maintaining consistent workspace isolation across all cache usage patterns. +type ScopedKey struct { + // WorkspaceID is the unique identifier for the workspace that owns this resource. + // This ensures that cache keys are isolated between different workspaces, + // preventing accidental data leakage or cache collisions. + WorkspaceID string + + // Key is the identifier for the resource within the workspace. + // This can be a user-provided name, system-generated ID, slug, or any other + // string identifier that uniquely identifies the resource within the workspace. + // + // The key is only guaranteed to be unique within the workspace context. + // Different workspaces may have resources with the same key value. + Key string +} From 7bda169b789e84a1bbc3680088c4a1f1b4fce3b8 Mon Sep 17 00:00:00 2001 From: chronark Date: Fri, 18 Jul 2025 20:08:56 +0200 Subject: [PATCH 2/9] feat: update openapi and accept a single namespace field everywhere --- go/apps/api/openapi/gen.go | 193 ++++-------------- go/apps/api/openapi/openapi.yaml | 130 ++++++------ .../v2_ratelimit_delete_override/200_test.go | 38 +++- .../v2_ratelimit_delete_override/400_test.go | 28 ++- .../v2_ratelimit_delete_override/401_test.go | 5 +- .../v2_ratelimit_delete_override/403_test.go | 7 +- .../v2_ratelimit_delete_override/404_test.go | 17 +- .../v2_ratelimit_delete_override/handler.go | 49 +++-- .../v2_ratelimit_get_override/200_test.go | 8 +- .../v2_ratelimit_get_override/400_test.go | 25 +-- .../v2_ratelimit_get_override/401_test.go | 5 +- .../v2_ratelimit_get_override/403_test.go | 7 +- .../v2_ratelimit_get_override/404_test.go | 21 +- .../v2_ratelimit_get_override/handler.go | 8 +- .../api/routes/v2_ratelimit_limit/handler.go | 9 +- .../v2_ratelimit_list_overrides/200_test.go | 6 +- .../v2_ratelimit_list_overrides/400_test.go | 15 +- .../v2_ratelimit_list_overrides/401_test.go | 3 +- .../v2_ratelimit_list_overrides/403_test.go | 2 +- .../v2_ratelimit_list_overrides/404_test.go | 4 +- .../v2_ratelimit_list_overrides/handler.go | 59 +++--- .../v2_ratelimit_set_override/200_test.go | 24 +-- .../v2_ratelimit_set_override/400_test.go | 59 +++--- .../v2_ratelimit_set_override/401_test.go | 9 +- .../v2_ratelimit_set_override/403_test.go | 8 +- .../v2_ratelimit_set_override/404_test.go | 18 +- .../v2_ratelimit_set_override/handler.go | 44 ++-- go/pkg/db/querier_generated.go | 4 +- .../db/queries/ratelimit_namespace_find.sql | 6 +- .../ratelimit_namespace_find.sql_generated.go | 23 +-- 30 files changed, 338 insertions(+), 496 deletions(-) diff --git a/go/apps/api/openapi/gen.go b/go/apps/api/openapi/gen.go index 136ac8ea83..5c7a05024c 100644 --- a/go/apps/api/openapi/gen.go +++ b/go/apps/api/openapi/gen.go @@ -1907,11 +1907,14 @@ type V2RatelimitDeleteOverrideRequestBody struct { // After deletion, any identifiers previously affected by this override will immediately revert to using the default rate limit for the namespace. Identifier string `json:"identifier"` - // NamespaceId The unique ID of the rate limit namespace containing the override. Either `namespaceId` or `namespaceName` must be provided, but not both. Using `namespaceId` is more precise and less prone to naming conflicts, making it ideal for automation and scripts. - NamespaceId *string `json:"namespaceId,omitempty"` - - // NamespaceName The name of the rate limit namespace containing the override. Either `namespaceId` or `namespaceName` must be provided, but not both. Using `namespaceName` is more human-readable and convenient for manual operations and configurations. - NamespaceName *string `json:"namespaceName,omitempty"` + // Namespace The rate limit namespace identifier. This can be either: + // - A namespace name (e.g., "api.requests", "auth.login") - human-readable names that are unique within your workspace + // - A namespace ID (e.g., "ns_1234567890abcdef") - system-generated unique identifiers + // + // The system will automatically detect whether you've provided a name or ID and perform the appropriate lookup. + // Names must start with a letter and can contain letters, numbers, underscores, dots, slashes, or hyphens. + // IDs follow the format "ns_" followed by an alphanumeric string. + Namespace string `json:"namespace"` } // V2RatelimitDeleteOverrideResponseBody defines model for V2RatelimitDeleteOverrideResponseBody. @@ -1942,20 +1945,16 @@ type V2RatelimitGetOverrideRequestBody struct { // This field is used to look up the specific override configuration for this pattern. Identifier string `json:"identifier"` - // NamespaceId The unique ID of the rate limit namespace. Either `namespaceId` or `namespaceName` must be provided, but not both. Using `namespaceId` is more precise and less prone to naming conflicts, making it ideal for scripts and automated operations. - NamespaceId *string `json:"namespaceId,omitempty"` - - // NamespaceName The name of the rate limit namespace. Either `namespaceId` or `namespaceName` must be provided, but not both. Using `namespaceName` is more human-readable and easier to work with for manual operations and configurations. - NamespaceName *string `json:"namespaceName,omitempty"` - union json.RawMessage + // Namespace The rate limit namespace identifier. This can be either: + // - A namespace name (e.g., "api.requests", "auth.login") - human-readable names that are unique within your workspace + // - A namespace ID (e.g., "ns_1234567890abcdef") - system-generated unique identifiers + // + // The system will automatically detect whether you've provided a name or ID and perform the appropriate lookup. + // Names must start with a letter and can contain letters, numbers, underscores, dots, slashes, or hyphens. + // IDs follow the format "ns_" followed by an alphanumeric string. + Namespace string `json:"namespace"` } -// V2RatelimitGetOverrideRequestBody0 defines model for . -type V2RatelimitGetOverrideRequestBody0 = interface{} - -// V2RatelimitGetOverrideRequestBody1 defines model for . -type V2RatelimitGetOverrideRequestBody1 = interface{} - // V2RatelimitGetOverrideResponseBody defines model for V2RatelimitGetOverrideResponseBody. type V2RatelimitGetOverrideResponseBody struct { Data RatelimitOverride `json:"data"` @@ -1991,11 +1990,13 @@ type V2RatelimitLimitRequestBody struct { // Consider system capacity, business requirements, and fair usage policies in limit determination. Limit int64 `json:"limit"` - // Namespace Identifies the rate limit category using hierarchical naming for organization and monitoring. - // Namespaces must start with a letter and can contain letters, numbers, underscores, dots, slashes, or hyphens. - // Use descriptive, hierarchical names like 'auth.login', 'api.requests', or 'media.uploads' for clear categorization. - // Namespaces must be unique within your workspace and support segmentation of different API operations. - // Consistent naming conventions across your application improve monitoring and debugging capabilities. + // Namespace The rate limit namespace identifier. This can be either: + // - A namespace name (e.g., "api.requests", "auth.login") - human-readable names that are unique within your workspace + // - A namespace ID (e.g., "ns_1234567890abcdef") - system-generated unique identifiers + // + // The system will automatically detect whether you've provided a name or ID and perform the appropriate lookup. + // Names must start with a letter and can contain letters, numbers, underscores, dots, slashes, or hyphens. + // IDs follow the format "ns_" followed by an alphanumeric string. Namespace string `json:"namespace"` } @@ -2021,11 +2022,14 @@ type V2RatelimitListOverridesRequestBody struct { // Results exceeding this limit will be paginated, with a cursor provided for fetching subsequent pages. Limit *int `json:"limit,omitempty"` - // NamespaceId The unique ID of the rate limit namespace to list overrides for. Either `namespaceId` or `namespaceName` must be provided, but not both. Using `namespaceId` guarantees you're targeting the exact namespace intended, even if names change over time. - NamespaceId *string `json:"namespaceId,omitempty"` - - // NamespaceName The name of the rate limit namespace to list overrides for. Either `namespaceId` or `namespaceName` must be provided, but not both. Using `namespaceName` is more human-readable and convenient for manual operations and dashboards. - NamespaceName *string `json:"namespaceName,omitempty"` + // Namespace The rate limit namespace identifier. This can be either: + // - A namespace name (e.g., "api.requests", "auth.login") - human-readable names that are unique within your workspace + // - A namespace ID (e.g., "ns_1234567890abcdef") - system-generated unique identifiers + // + // The system will automatically detect whether you've provided a name or ID and perform the appropriate lookup. + // Names must start with a letter and can contain letters, numbers, underscores, dots, slashes, or hyphens. + // IDs follow the format "ns_" followed by an alphanumeric string. + Namespace string `json:"namespace"` } // V2RatelimitListOverridesResponseBody defines model for V2RatelimitListOverridesResponseBody. @@ -2080,11 +2084,14 @@ type V2RatelimitSetOverrideRequestBody struct { // This limit entirely replaces the default limit for matching identifiers. Limit int64 `json:"limit"` - // NamespaceId The unique ID of the rate limit namespace. Either `namespaceId` or `namespaceName` must be provided, but not both. Using `namespaceId` guarantees you're targeting the exact namespace intended, even if names change, making it ideal for automation and scripts. - NamespaceId *string `json:"namespaceId,omitempty"` - - // NamespaceName The name of the rate limit namespace. Either `namespaceId` or `namespaceName` must be provided, but not both. Using `namespaceName` is more human-readable and convenient for manual operations and configurations. - NamespaceName *string `json:"namespaceName,omitempty"` + // Namespace The rate limit namespace identifier. This can be either: + // - A namespace name (e.g., "api.requests", "auth.login") - human-readable names that are unique within your workspace + // - A namespace ID (e.g., "ns_1234567890abcdef") - system-generated unique identifiers + // + // The system will automatically detect whether you've provided a name or ID and perform the appropriate lookup. + // Names must start with a letter and can contain letters, numbers, underscores, dots, slashes, or hyphens. + // IDs follow the format "ns_" followed by an alphanumeric string. + Namespace string `json:"namespace"` } // V2RatelimitSetOverrideResponseBody defines model for V2RatelimitSetOverrideResponseBody. @@ -2366,125 +2373,3 @@ func (t *V2KeysGetKeyRequestBody) UnmarshalJSON(b []byte) error { return err } - -// AsV2RatelimitGetOverrideRequestBody0 returns the union data inside the V2RatelimitGetOverrideRequestBody as a V2RatelimitGetOverrideRequestBody0 -func (t V2RatelimitGetOverrideRequestBody) AsV2RatelimitGetOverrideRequestBody0() (V2RatelimitGetOverrideRequestBody0, error) { - var body V2RatelimitGetOverrideRequestBody0 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromV2RatelimitGetOverrideRequestBody0 overwrites any union data inside the V2RatelimitGetOverrideRequestBody as the provided V2RatelimitGetOverrideRequestBody0 -func (t *V2RatelimitGetOverrideRequestBody) FromV2RatelimitGetOverrideRequestBody0(v V2RatelimitGetOverrideRequestBody0) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeV2RatelimitGetOverrideRequestBody0 performs a merge with any union data inside the V2RatelimitGetOverrideRequestBody, using the provided V2RatelimitGetOverrideRequestBody0 -func (t *V2RatelimitGetOverrideRequestBody) MergeV2RatelimitGetOverrideRequestBody0(v V2RatelimitGetOverrideRequestBody0) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsV2RatelimitGetOverrideRequestBody1 returns the union data inside the V2RatelimitGetOverrideRequestBody as a V2RatelimitGetOverrideRequestBody1 -func (t V2RatelimitGetOverrideRequestBody) AsV2RatelimitGetOverrideRequestBody1() (V2RatelimitGetOverrideRequestBody1, error) { - var body V2RatelimitGetOverrideRequestBody1 - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromV2RatelimitGetOverrideRequestBody1 overwrites any union data inside the V2RatelimitGetOverrideRequestBody as the provided V2RatelimitGetOverrideRequestBody1 -func (t *V2RatelimitGetOverrideRequestBody) FromV2RatelimitGetOverrideRequestBody1(v V2RatelimitGetOverrideRequestBody1) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeV2RatelimitGetOverrideRequestBody1 performs a merge with any union data inside the V2RatelimitGetOverrideRequestBody, using the provided V2RatelimitGetOverrideRequestBody1 -func (t *V2RatelimitGetOverrideRequestBody) MergeV2RatelimitGetOverrideRequestBody1(v V2RatelimitGetOverrideRequestBody1) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -func (t V2RatelimitGetOverrideRequestBody) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - if err != nil { - return nil, err - } - object := make(map[string]json.RawMessage) - if t.union != nil { - err = json.Unmarshal(b, &object) - if err != nil { - return nil, err - } - } - - object["identifier"], err = json.Marshal(t.Identifier) - if err != nil { - return nil, fmt.Errorf("error marshaling 'identifier': %w", err) - } - - if t.NamespaceId != nil { - object["namespaceId"], err = json.Marshal(t.NamespaceId) - if err != nil { - return nil, fmt.Errorf("error marshaling 'namespaceId': %w", err) - } - } - - if t.NamespaceName != nil { - object["namespaceName"], err = json.Marshal(t.NamespaceName) - if err != nil { - return nil, fmt.Errorf("error marshaling 'namespaceName': %w", err) - } - } - b, err = json.Marshal(object) - return b, err -} - -func (t *V2RatelimitGetOverrideRequestBody) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) - if err != nil { - return err - } - object := make(map[string]json.RawMessage) - err = json.Unmarshal(b, &object) - if err != nil { - return err - } - - if raw, found := object["identifier"]; found { - err = json.Unmarshal(raw, &t.Identifier) - if err != nil { - return fmt.Errorf("error reading 'identifier': %w", err) - } - } - - if raw, found := object["namespaceId"]; found { - err = json.Unmarshal(raw, &t.NamespaceId) - if err != nil { - return fmt.Errorf("error reading 'namespaceId': %w", err) - } - } - - if raw, found := object["namespaceName"]; found { - err = json.Unmarshal(raw, &t.NamespaceName) - if err != nil { - return fmt.Errorf("error reading 'namespaceName': %w", err) - } - } - - return err -} diff --git a/go/apps/api/openapi/openapi.yaml b/go/apps/api/openapi/openapi.yaml index 8b0297c3f5..effe0c1e1b 100644 --- a/go/apps/api/openapi/openapi.yaml +++ b/go/apps/api/openapi/openapi.yaml @@ -1813,21 +1813,19 @@ components: - Prioritizing important clients with higher limits additionalProperties: false properties: - namespaceId: - description: - The unique ID of the rate limit namespace. Either `namespaceId` - or `namespaceName` must be provided, but not both. Using `namespaceId` guarantees - you're targeting the exact namespace intended, even if names change, making - it ideal for automation and scripts. + namespace: type: string minLength: 1 maxLength: 255 - namespaceName: - description: - The name of the rate limit namespace. Either `namespaceId` or - `namespaceName` must be provided, but not both. Using `namespaceName` is more - human-readable and convenient for manual operations and configurations. - type: string + description: | + The rate limit namespace identifier. This can be either: + - A namespace name (e.g., "api.requests", "auth.login") - human-readable names that are unique within your workspace + - A namespace ID (e.g., "ns_1234567890abcdef") - system-generated unique identifiers + + The system will automatically detect whether you've provided a name or ID and perform the appropriate lookup. + Names must start with a letter and can contain letters, numbers, underscores, dots, slashes, or hyphens. + IDs follow the format "ns_" followed by an alphanumeric string. + example: api.requests duration: description: |- The duration in milliseconds for the rate limit window. This defines how long the rate limit counter accumulates before resetting to zero. @@ -1872,6 +1870,7 @@ components: type: integer minimum: 0 required: + - namespace - identifier - limit - duration @@ -1914,23 +1913,19 @@ components: - Retrieving override settings for modification additionalProperties: false properties: - namespaceId: - description: - The unique ID of the rate limit namespace. Either `namespaceId` - or `namespaceName` must be provided, but not both. Using `namespaceId` is - more precise and less prone to naming conflicts, making it ideal for scripts - and automated operations. - type: string - minLength: 1 - maxLength: 255 - namespaceName: - description: - The name of the rate limit namespace. Either `namespaceId` or - `namespaceName` must be provided, but not both. Using `namespaceName` is more - human-readable and easier to work with for manual operations and configurations. + namespace: type: string minLength: 1 maxLength: 255 + description: | + The rate limit namespace identifier. This can be either: + - A namespace name (e.g., "api.requests", "auth.login") - human-readable names that are unique within your workspace + - A namespace ID (e.g., "ns_1234567890abcdef") - system-generated unique identifiers + + The system will automatically detect whether you've provided a name or ID and perform the appropriate lookup. + Names must start with a letter and can contain letters, numbers, underscores, dots, slashes, or hyphens. + IDs follow the format "ns_" followed by an alphanumeric string. + example: api.requests identifier: description: |- The exact identifier pattern for the override you want to retrieve. This must match exactly as it was specified when creating the override. @@ -1945,15 +1940,9 @@ components: minLength: 1 maxLength: 255 required: + - namespace - identifier type: object - oneOf: - - required: - - namespaceName - - identifier - - required: - - namespaceId - - identifier V2RatelimitGetOverrideResponseBody: type: object required: @@ -1967,22 +1956,19 @@ components: V2RatelimitListOverridesRequestBody: additionalProperties: false properties: - namespaceId: - description: - The unique ID of the rate limit namespace to list overrides - for. Either `namespaceId` or `namespaceName` must be provided, but not both. - Using `namespaceId` guarantees you're targeting the exact namespace intended, - even if names change over time. + namespace: type: string minLength: 1 maxLength: 255 - namespaceName: - description: - The name of the rate limit namespace to list overrides for. - Either `namespaceId` or `namespaceName` must be provided, but not both. Using - `namespaceName` is more human-readable and convenient for manual operations - and dashboards. - type: string + description: | + The rate limit namespace identifier. This can be either: + - A namespace name (e.g., "api.requests", "auth.login") - human-readable names that are unique within your workspace + - A namespace ID (e.g., "ns_1234567890abcdef") - system-generated unique identifiers + + The system will automatically detect whether you've provided a name or ID and perform the appropriate lookup. + Names must start with a letter and can contain letters, numbers, underscores, dots, slashes, or hyphens. + IDs follow the format "ns_" followed by an alphanumeric string. + example: api.requests cursor: description: Pagination cursor from a previous response. Include this when @@ -2003,6 +1989,8 @@ components: default: 10 minimum: 1 maximum: 100 + required: + - namespace type: object RatelimitListOverridesResponseData: type: array @@ -2026,15 +2014,16 @@ components: namespace: type: string minLength: 1 - maxLength: 255 # Reasonable upper bound for namespace identifiers - pattern: "^[a-zA-Z][a-zA-Z0-9_./-]*$" + maxLength: 255 description: | - Identifies the rate limit category using hierarchical naming for organization and monitoring. - Namespaces must start with a letter and can contain letters, numbers, underscores, dots, slashes, or hyphens. - Use descriptive, hierarchical names like 'auth.login', 'api.requests', or 'media.uploads' for clear categorization. - Namespaces must be unique within your workspace and support segmentation of different API operations. - Consistent naming conventions across your application improve monitoring and debugging capabilities. - example: sms.sign_up + The rate limit namespace identifier. This can be either: + - A namespace name (e.g., "api.requests", "auth.login") - human-readable names that are unique within your workspace + - A namespace ID (e.g., "ns_1234567890abcdef") - system-generated unique identifiers + + The system will automatically detect whether you've provided a name or ID and perform the appropriate lookup. + Names must start with a letter and can contain letters, numbers, underscores, dots, slashes, or hyphens. + IDs follow the format "ns_" followed by an alphanumeric string. + example: api.requests cost: type: integer format: int64 @@ -2167,24 +2156,19 @@ components: Once deleted, the override cannot be recovered, and the operation takes effect immediately. additionalProperties: false properties: - namespaceId: - description: - The unique ID of the rate limit namespace containing the override. - Either `namespaceId` or `namespaceName` must be provided, but not both. Using - `namespaceId` is more precise and less prone to naming conflicts, making - it ideal for automation and scripts. - type: string - minLength: 1 - maxLength: 255 - namespaceName: - description: - The name of the rate limit namespace containing the override. - Either `namespaceId` or `namespaceName` must be provided, but not both. Using - `namespaceName` is more human-readable and convenient for manual operations - and configurations. + namespace: type: string minLength: 1 maxLength: 255 + description: | + The rate limit namespace identifier. This can be either: + - A namespace name (e.g., "api.requests", "auth.login") - human-readable names that are unique within your workspace + - A namespace ID (e.g., "ns_1234567890abcdef") - system-generated unique identifiers + + The system will automatically detect whether you've provided a name or ID and perform the appropriate lookup. + Names must start with a letter and can contain letters, numbers, underscores, dots, slashes, or hyphens. + IDs follow the format "ns_" followed by an alphanumeric string. + example: api.requests identifier: description: |- The exact identifier pattern of the override to delete. This must match exactly as it was specified when creating the override. @@ -2199,6 +2183,7 @@ components: minLength: 1 maxLength: 255 required: + - namespace - identifier type: object RatelimitDeleteOverrideResponseData: @@ -5411,6 +5396,13 @@ paths: limit: 50 duration: 3600000 cost: 5 + usingNamespaceId: + summary: Using namespace ID instead of name + value: + namespace: ns_1234567890abcdef + identifier: user_xyz789 + limit: 200 + duration: 60000 required: true responses: "200": diff --git a/go/apps/api/routes/v2_ratelimit_delete_override/200_test.go b/go/apps/api/routes/v2_ratelimit_delete_override/200_test.go index a6358e9806..0899bcbed1 100644 --- a/go/apps/api/routes/v2_ratelimit_delete_override/200_test.go +++ b/go/apps/api/routes/v2_ratelimit_delete_override/200_test.go @@ -63,8 +63,8 @@ func TestDeleteOverrideSuccessfully(t *testing.T) { // Test deleting by namespace name t.Run("delete by namespace name", func(t *testing.T) { req := handler.Request{ - NamespaceName: &namespaceName, - Identifier: identifier, + Namespace: namespaceName, + Identifier: identifier, } res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) @@ -79,4 +79,38 @@ func TestDeleteOverrideSuccessfully(t *testing.T) { require.NoError(t, err) require.True(t, override.DeletedAtM.Valid, "Override should be marked as deleted") }) + + // Test deleting by namespace ID + t.Run("delete by namespace ID", func(t *testing.T) { + // Create another override to test ID-based deletion + identifier2 := "test_identifier_2" + overrideID2 := uid.New(uid.RatelimitOverridePrefix) + err = db.Query.InsertRatelimitOverride(ctx, h.DB.RW(), db.InsertRatelimitOverrideParams{ + ID: overrideID2, + WorkspaceID: h.Resources().UserWorkspace.ID, + NamespaceID: namespaceID, + Identifier: identifier2, + Limit: 10, + Duration: 1000, + CreatedAt: time.Now().UnixMilli(), + }) + require.NoError(t, err) + + req := handler.Request{ + Namespace: namespaceID, + Identifier: identifier2, + } + + res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) + require.Equal(t, 200, res.Status, "expected 200, received: %s", res.RawBody) + + // Verify the override was deleted (check soft delete) + override, err := db.Query.FindRatelimitOverrideByID(ctx, h.DB.RO(), db.FindRatelimitOverrideByIDParams{ + WorkspaceID: h.Resources().UserWorkspace.ID, + OverrideID: overrideID2, + }) + + require.NoError(t, err) + require.True(t, override.DeletedAtM.Valid, "Override should be marked as deleted") + }) } diff --git a/go/apps/api/routes/v2_ratelimit_delete_override/400_test.go b/go/apps/api/routes/v2_ratelimit_delete_override/400_test.go index 8587200d45..beb6e5382b 100644 --- a/go/apps/api/routes/v2_ratelimit_delete_override/400_test.go +++ b/go/apps/api/routes/v2_ratelimit_delete_override/400_test.go @@ -8,7 +8,6 @@ import ( "github.com/stretchr/testify/require" "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_ratelimit_delete_override" - "github.com/unkeyed/unkey/go/pkg/ptr" "github.com/unkeyed/unkey/go/pkg/testutil" "github.com/unkeyed/unkey/go/pkg/uid" ) @@ -49,7 +48,7 @@ func TestBadRequests(t *testing.T) { t.Run("missing identifier", func(t *testing.T) { req := openapi.V2RatelimitDeleteOverrideRequestBody{ - NamespaceId: ptr.P("test_namespace_id"), + Namespace: "test_namespace_id", } res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse](h, route, headers, req) @@ -67,8 +66,8 @@ func TestBadRequests(t *testing.T) { t.Run("empty identifier", func(t *testing.T) { req := openapi.V2RatelimitDeleteOverrideRequestBody{ - NamespaceId: ptr.P("test_namespace_id"), - Identifier: "", + Namespace: "test_namespace_id", + Identifier: "", } res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse](h, route, headers, req) @@ -84,11 +83,10 @@ func TestBadRequests(t *testing.T) { require.Greater(t, len(res.Body.Error.Errors), 0) }) - t.Run("neither namespace ID nor name provided", func(t *testing.T) { + t.Run("namespace not provided", func(t *testing.T) { req := openapi.V2RatelimitDeleteOverrideRequestBody{ - NamespaceId: nil, - NamespaceName: nil, - Identifier: "user_123", + Namespace: "", + Identifier: "user_123", } res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse](h, route, headers, req) @@ -97,11 +95,11 @@ func TestBadRequests(t *testing.T) { require.NotNil(t, res.Body) require.Equal(t, "https://unkey.com/docs/api-reference/errors-v2/unkey/application/invalid_input", res.Body.Error.Type) - require.Equal(t, "You must provide either a namespace ID or name.", res.Body.Error.Detail) + require.Equal(t, "POST request body for '/v2/ratelimit.deleteOverride' failed to validate schema", res.Body.Error.Detail) require.Equal(t, http.StatusBadRequest, res.Body.Error.Status) require.Equal(t, "Bad Request", res.Body.Error.Title) require.NotEmpty(t, res.Body.Meta.RequestId) - require.Equal(t, len(res.Body.Error.Errors), 0) + require.Greater(t, len(res.Body.Error.Errors), 0) }) t.Run("missing authorization header", func(t *testing.T) { @@ -110,10 +108,9 @@ func TestBadRequests(t *testing.T) { // No Authorization header } - namespaceName := uid.New("test") req := handler.Request{ - NamespaceName: &namespaceName, - Identifier: "test_identifier", + Namespace: uid.New("test"), + Identifier: "test_identifier", } res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) @@ -127,10 +124,9 @@ func TestBadRequests(t *testing.T) { "Authorization": {"malformed_header"}, } - namespaceName := uid.New("test") req := handler.Request{ - NamespaceName: &namespaceName, - Identifier: "test_identifier", + Namespace: uid.New("test"), + Identifier: "test_identifier", } res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) diff --git a/go/apps/api/routes/v2_ratelimit_delete_override/401_test.go b/go/apps/api/routes/v2_ratelimit_delete_override/401_test.go index c34f054203..b9f1430d1f 100644 --- a/go/apps/api/routes/v2_ratelimit_delete_override/401_test.go +++ b/go/apps/api/routes/v2_ratelimit_delete_override/401_test.go @@ -29,10 +29,9 @@ func TestUnauthorizedAccess(t *testing.T) { "Authorization": {"Bearer invalid_token"}, } - namespaceName := uid.New("test") req := handler.Request{ - NamespaceName: &namespaceName, - Identifier: "test_identifier", + Namespace: uid.New("test"), + Identifier: "test_identifier", } res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) diff --git a/go/apps/api/routes/v2_ratelimit_delete_override/403_test.go b/go/apps/api/routes/v2_ratelimit_delete_override/403_test.go index 4e0ab744b6..37651dac45 100644 --- a/go/apps/api/routes/v2_ratelimit_delete_override/403_test.go +++ b/go/apps/api/routes/v2_ratelimit_delete_override/403_test.go @@ -21,11 +21,10 @@ func TestWorkspacePermissions(t *testing.T) { // Create a namespace in the default workspace namespaceID := uid.New(uid.RatelimitNamespacePrefix) - namespaceName := uid.New("test") err := db.Query.InsertRatelimitNamespace(ctx, h.DB.RW(), db.InsertRatelimitNamespaceParams{ ID: namespaceID, WorkspaceID: h.Resources().UserWorkspace.ID, // Use the default workspace - Name: namespaceName, + Name: uid.New("test"), CreatedAt: time.Now().UnixMilli(), }) require.NoError(t, err) @@ -66,8 +65,8 @@ func TestWorkspacePermissions(t *testing.T) { // Try to delete an override using a namespace from the default workspace // but with a key from a different workspace req := handler.Request{ - NamespaceId: &namespaceID, - Identifier: identifier, + Namespace: namespaceID, + Identifier: identifier, } res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse](h, route, headers, req) diff --git a/go/apps/api/routes/v2_ratelimit_delete_override/404_test.go b/go/apps/api/routes/v2_ratelimit_delete_override/404_test.go index efe7a1d5ff..9b78bea667 100644 --- a/go/apps/api/routes/v2_ratelimit_delete_override/404_test.go +++ b/go/apps/api/routes/v2_ratelimit_delete_override/404_test.go @@ -21,11 +21,10 @@ func TestNotFound(t *testing.T) { // Create a namespace but no override namespaceID := uid.New("test_ns") - namespaceName := uid.New("test") err := db.Query.InsertRatelimitNamespace(ctx, h.DB.RW(), db.InsertRatelimitNamespaceParams{ ID: namespaceID, WorkspaceID: h.Resources().UserWorkspace.ID, - Name: namespaceName, + Name: uid.New("test"), CreatedAt: time.Now().UnixMilli(), }) require.NoError(t, err) @@ -51,8 +50,8 @@ func TestNotFound(t *testing.T) { t.Run("override not found", func(t *testing.T) { req := handler.Request{ - NamespaceId: &namespaceID, - Identifier: "non_existent_identifier", + Namespace: namespaceID, + Identifier: "non_existent_identifier", } res := testutil.CallRoute[handler.Request, openapi.NotFoundErrorResponse](h, route, headers, req) @@ -64,10 +63,9 @@ func TestNotFound(t *testing.T) { // Test with non-existent namespace t.Run("namespace not found", func(t *testing.T) { - nonExistentNamespaceId := "ns_nonexistent" req := handler.Request{ - NamespaceId: &nonExistentNamespaceId, - Identifier: "some_identifier", + Namespace: "ns_nonexistent", + Identifier: "some_identifier", } res := testutil.CallRoute[handler.Request, openapi.NotFoundErrorResponse](h, route, headers, req) @@ -79,10 +77,9 @@ func TestNotFound(t *testing.T) { // Test with non-existent namespace name t.Run("namespace name not found", func(t *testing.T) { - nonExistentNamespaceName := "nonexistent_namespace" req := handler.Request{ - NamespaceName: &nonExistentNamespaceName, - Identifier: "some_identifier", + Namespace: "nonexistent_namespace", + Identifier: "some_identifier", } res := testutil.CallRoute[handler.Request, openapi.NotFoundErrorResponse](h, route, headers, req) diff --git a/go/apps/api/routes/v2_ratelimit_delete_override/handler.go b/go/apps/api/routes/v2_ratelimit_delete_override/handler.go index dce3de6c48..e9ff3db0c8 100644 --- a/go/apps/api/routes/v2_ratelimit_delete_override/handler.go +++ b/go/apps/api/routes/v2_ratelimit_delete_override/handler.go @@ -54,18 +54,33 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { return err } - namespace, err := getNamespace(ctx, h, auth.AuthorizedWorkspaceID, req) - if db.IsNotFound(err) { - return fault.New("namespace not found", - fault.Code(codes.Data.RatelimitNamespace.NotFound.URN()), - fault.Internal("namespace not found"), - fault.Public("This namespace does not exist."), - ) - } + // Use the namespace field directly - it can be either name or ID + namespaceKey := req.Namespace + + response, err := db.Query.FindRatelimitNamespace(ctx, h.DB.RO(), db.FindRatelimitNamespaceParams{ + WorkspaceID: auth.AuthorizedWorkspaceID, + Namespace: namespaceKey, + }) if err != nil { + if db.IsNotFound(err) { + return fault.New("namespace not found", + fault.Code(codes.Data.RatelimitNamespace.NotFound.URN()), + fault.Internal("namespace not found"), + fault.Public("This namespace does not exist."), + ) + } return err } + namespace := db.RatelimitNamespace{ + ID: response.ID, + WorkspaceID: response.WorkspaceID, + Name: response.Name, + CreatedAtM: response.CreatedAtM, + UpdatedAtM: response.UpdatedAtM, + DeletedAtM: response.DeletedAtM, + } + if namespace.WorkspaceID != auth.AuthorizedWorkspaceID { return fault.New("namespace not found", fault.Code(codes.Data.RatelimitNamespace.NotFound.URN()), @@ -180,21 +195,3 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { Data: openapi.RatelimitDeleteOverrideResponseData{}, }) } - -func getNamespace(ctx context.Context, h *Handler, workspaceID string, req Request) (db.RatelimitNamespace, error) { - switch { - case req.NamespaceId != nil: - return db.Query.FindRatelimitNamespaceByID(ctx, h.DB.RO(), *req.NamespaceId) - case req.NamespaceName != nil: - return db.Query.FindRatelimitNamespaceByName(ctx, h.DB.RO(), db.FindRatelimitNamespaceByNameParams{ - WorkspaceID: workspaceID, - Name: *req.NamespaceName, - }) - } - - return db.RatelimitNamespace{}, fault.New("missing namespace id or name", - fault.Code(codes.App.Validation.InvalidInput.URN()), - fault.Internal("missing namespace id or name"), - fault.Public("You must provide either a namespace ID or name."), - ) -} diff --git a/go/apps/api/routes/v2_ratelimit_get_override/200_test.go b/go/apps/api/routes/v2_ratelimit_get_override/200_test.go index 12dec85e4e..7bfd8ffccc 100644 --- a/go/apps/api/routes/v2_ratelimit_get_override/200_test.go +++ b/go/apps/api/routes/v2_ratelimit_get_override/200_test.go @@ -65,8 +65,8 @@ func TestGetOverrideSuccessfully(t *testing.T) { // Test getting by namespace name t.Run("get by namespace name", func(t *testing.T) { req := handler.Request{ - NamespaceName: &namespaceName, - Identifier: identifier, + Namespace: namespaceName, + Identifier: identifier, } res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) @@ -82,8 +82,8 @@ func TestGetOverrideSuccessfully(t *testing.T) { // Test getting by namespace ID t.Run("get by namespace ID", func(t *testing.T) { req := handler.Request{ - NamespaceId: &namespaceID, - Identifier: identifier, + Namespace: namespaceID, + Identifier: identifier, } res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) diff --git a/go/apps/api/routes/v2_ratelimit_get_override/400_test.go b/go/apps/api/routes/v2_ratelimit_get_override/400_test.go index f0c3d25d97..669d75a85b 100644 --- a/go/apps/api/routes/v2_ratelimit_get_override/400_test.go +++ b/go/apps/api/routes/v2_ratelimit_get_override/400_test.go @@ -9,7 +9,6 @@ import ( "github.com/stretchr/testify/require" "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_ratelimit_get_override" - "github.com/unkeyed/unkey/go/pkg/ptr" "github.com/unkeyed/unkey/go/pkg/testutil" "github.com/unkeyed/unkey/go/pkg/uid" ) @@ -50,7 +49,7 @@ func TestBadRequests(t *testing.T) { t.Run("missing identifier", func(t *testing.T) { req := openapi.V2RatelimitGetOverrideRequestBody{ - NamespaceId: ptr.P("not_empty"), + Namespace: "not_empty", } res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse](h, route, headers, req) @@ -68,9 +67,8 @@ func TestBadRequests(t *testing.T) { t.Run("empty identifier", func(t *testing.T) { req := openapi.V2RatelimitGetOverrideRequestBody{ - NamespaceId: ptr.P("not_empty"), - NamespaceName: nil, - Identifier: "", + Namespace: "not_empty", + Identifier: "", } res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse](h, route, headers, req) @@ -88,9 +86,8 @@ func TestBadRequests(t *testing.T) { t.Run("neither namespace ID nor name provided", func(t *testing.T) { req := openapi.V2RatelimitGetOverrideRequestBody{ - NamespaceId: nil, - NamespaceName: nil, - Identifier: "user_123", + Namespace: "", + Identifier: "user_123", } res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse](h, route, headers, req) @@ -103,7 +100,7 @@ func TestBadRequests(t *testing.T) { require.Equal(t, http.StatusBadRequest, res.Body.Error.Status) require.Equal(t, "Bad Request", res.Body.Error.Title) require.NotEmpty(t, res.Body.Meta.RequestId) - require.Equal(t, len(res.Body.Error.Errors), 3) + require.Greater(t, len(res.Body.Error.Errors), 0) }) t.Run("missing authorization header", func(t *testing.T) { @@ -112,10 +109,9 @@ func TestBadRequests(t *testing.T) { // No Authorization header } - namespaceName := uid.New("test") req := handler.Request{ - NamespaceName: &namespaceName, - Identifier: "test_identifier", + Namespace: uid.New("test"), + Identifier: "test_identifier", } res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) @@ -129,10 +125,9 @@ func TestBadRequests(t *testing.T) { "Authorization": {"malformed_header"}, } - namespaceName := uid.New("test") req := handler.Request{ - NamespaceName: &namespaceName, - Identifier: "test_identifier", + Namespace: uid.New("test"), + Identifier: "test_identifier", } res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) diff --git a/go/apps/api/routes/v2_ratelimit_get_override/401_test.go b/go/apps/api/routes/v2_ratelimit_get_override/401_test.go index e4f3b62abf..60e5a706df 100644 --- a/go/apps/api/routes/v2_ratelimit_get_override/401_test.go +++ b/go/apps/api/routes/v2_ratelimit_get_override/401_test.go @@ -28,10 +28,9 @@ func TestUnauthorizedAccess(t *testing.T) { "Authorization": {"Bearer invalid_token"}, } - namespaceName := uid.New("test") req := handler.Request{ - NamespaceName: &namespaceName, - Identifier: "test_identifier", + Namespace: uid.New("test"), + Identifier: "test_identifier", } res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) diff --git a/go/apps/api/routes/v2_ratelimit_get_override/403_test.go b/go/apps/api/routes/v2_ratelimit_get_override/403_test.go index c076d5bc28..3101d46515 100644 --- a/go/apps/api/routes/v2_ratelimit_get_override/403_test.go +++ b/go/apps/api/routes/v2_ratelimit_get_override/403_test.go @@ -21,11 +21,10 @@ func TestWorkspacePermissions(t *testing.T) { // Create a namespace namespaceID := uid.New(uid.RatelimitNamespacePrefix) - namespaceName := uid.New("test") err := db.Query.InsertRatelimitNamespace(ctx, h.DB.RW(), db.InsertRatelimitNamespaceParams{ ID: namespaceID, WorkspaceID: h.Resources().UserWorkspace.ID, // Use the default workspace - Name: namespaceName, + Name: uid.New("test"), CreatedAt: time.Now().UnixMilli(), }) require.NoError(t, err) @@ -63,8 +62,8 @@ func TestWorkspacePermissions(t *testing.T) { // Try to access the override with a key from a different workspace req := handler.Request{ - NamespaceId: &namespaceID, - Identifier: identifier, + Namespace: namespaceID, + Identifier: identifier, } res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse](h, route, headers, req) diff --git a/go/apps/api/routes/v2_ratelimit_get_override/404_test.go b/go/apps/api/routes/v2_ratelimit_get_override/404_test.go index b47f697a98..fcb4d184a2 100644 --- a/go/apps/api/routes/v2_ratelimit_get_override/404_test.go +++ b/go/apps/api/routes/v2_ratelimit_get_override/404_test.go @@ -21,11 +21,10 @@ func TestOverrideNotFound(t *testing.T) { // Create a namespace but no override namespaceID := uid.New("test_ns") - namespaceName := uid.New("test") err := db.Query.InsertRatelimitNamespace(ctx, h.DB.RW(), db.InsertRatelimitNamespaceParams{ ID: namespaceID, WorkspaceID: h.Resources().UserWorkspace.ID, - Name: namespaceName, + Name: uid.New("test"), CreatedAt: time.Now().UnixMilli(), }) require.NoError(t, err) @@ -48,8 +47,8 @@ func TestOverrideNotFound(t *testing.T) { // Test with non-existent identifier t.Run("override not found", func(t *testing.T) { req := handler.Request{ - NamespaceId: &namespaceID, - Identifier: "non_existent_identifier", + Namespace: namespaceID, + Identifier: "non_existent_identifier", } res := testutil.CallRoute[handler.Request, openapi.NotFoundErrorResponse](h, route, headers, req) @@ -61,10 +60,9 @@ func TestOverrideNotFound(t *testing.T) { // Test with non-existent namespace t.Run("namespace not found", func(t *testing.T) { - nonExistentNamespaceId := "ns_nonexistent" req := handler.Request{ - NamespaceId: &nonExistentNamespaceId, - Identifier: "some_identifier", + Namespace: "ns_nonexistent", + Identifier: "some_identifier", } res := testutil.CallRoute[handler.Request, openapi.NotFoundErrorResponse](h, route, headers, req) @@ -75,10 +73,9 @@ func TestOverrideNotFound(t *testing.T) { // Test with non-existent namespace name t.Run("namespace name not found", func(t *testing.T) { - nonExistentNamespaceName := "nonexistent_namespace" req := handler.Request{ - NamespaceName: &nonExistentNamespaceName, - Identifier: "some_identifier", + Namespace: "nonexistent_namespace", + Identifier: "some_identifier", } res := testutil.CallRoute[handler.Request, openapi.NotFoundErrorResponse](h, route, headers, req) @@ -102,8 +99,8 @@ func TestOverrideNotFound(t *testing.T) { nonExistentIdentifier := "nonexistent_identifier" req := handler.Request{ - NamespaceId: &namespaceID, - Identifier: nonExistentIdentifier, + Namespace: namespaceID, + Identifier: nonExistentIdentifier, } res := testutil.CallRoute[handler.Request, openapi.NotFoundErrorResponse](h, route, http.Header{ diff --git a/go/apps/api/routes/v2_ratelimit_get_override/handler.go b/go/apps/api/routes/v2_ratelimit_get_override/handler.go index b4ae042245..a59e4b543a 100644 --- a/go/apps/api/routes/v2_ratelimit_get_override/handler.go +++ b/go/apps/api/routes/v2_ratelimit_get_override/handler.go @@ -2,7 +2,6 @@ package handler import ( "context" - "database/sql" "encoding/json" "net/http" "strings" @@ -15,7 +14,6 @@ import ( "github.com/unkeyed/unkey/go/pkg/fault" "github.com/unkeyed/unkey/go/pkg/match" "github.com/unkeyed/unkey/go/pkg/otel/logging" - "github.com/unkeyed/unkey/go/pkg/ptr" "github.com/unkeyed/unkey/go/pkg/rbac" "github.com/unkeyed/unkey/go/pkg/zen" ) @@ -54,10 +52,12 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { return err } + // Use the namespace field directly - it can be either name or ID + namespaceKey := req.Namespace + response, err := db.Query.FindRatelimitNamespace(ctx, h.DB.RO(), db.FindRatelimitNamespaceParams{ WorkspaceID: auth.AuthorizedWorkspaceID, - Name: sql.NullString{String: ptr.SafeDeref(req.NamespaceName), Valid: req.NamespaceName != nil}, - ID: sql.NullString{String: ptr.SafeDeref(req.NamespaceId), Valid: req.NamespaceId != nil}, + Namespace: namespaceKey, }) if err != nil { if db.IsNotFound(err) { diff --git a/go/apps/api/routes/v2_ratelimit_limit/handler.go b/go/apps/api/routes/v2_ratelimit_limit/handler.go index 03b1471787..09470ae693 100644 --- a/go/apps/api/routes/v2_ratelimit_limit/handler.go +++ b/go/apps/api/routes/v2_ratelimit_limit/handler.go @@ -2,7 +2,6 @@ package v2RatelimitLimit import ( "context" - "database/sql" "encoding/json" "net/http" "strconv" @@ -69,14 +68,16 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { cost = *req.Cost } + // Use the namespace field directly - it can be either name or ID + namespaceKey := req.Namespace + ctx, span := tracing.Start(ctx, "FindRatelimitNamespace") namespace, err := h.RatelimitNamespaceCache.SWR(ctx, - cache.ScopedKey{WorkspaceID: auth.AuthorizedWorkspaceID, Key: req.Namespace}, + cache.ScopedKey{WorkspaceID: auth.AuthorizedWorkspaceID, Key: namespaceKey}, func(ctx context.Context) (db.FindRatelimitNamespace, error) { response, err := db.Query.FindRatelimitNamespace(ctx, h.DB.RO(), db.FindRatelimitNamespaceParams{ WorkspaceID: auth.AuthorizedWorkspaceID, - Name: sql.NullString{String: req.Namespace, Valid: true}, - ID: sql.NullString{String: "", Valid: false}, + Namespace: namespaceKey, }) result := db.FindRatelimitNamespace{} // nolint:exhaustruct if err != nil { diff --git a/go/apps/api/routes/v2_ratelimit_list_overrides/200_test.go b/go/apps/api/routes/v2_ratelimit_list_overrides/200_test.go index 6546aa2d09..ad897413f0 100644 --- a/go/apps/api/routes/v2_ratelimit_list_overrides/200_test.go +++ b/go/apps/api/routes/v2_ratelimit_list_overrides/200_test.go @@ -64,7 +64,7 @@ func TestListOverridesSuccessfully(t *testing.T) { // Test getting by namespace name t.Run("get by namespace name", func(t *testing.T) { req := handler.Request{ - NamespaceName: &namespaceName, + Namespace: namespaceName, } res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) @@ -81,7 +81,7 @@ func TestListOverridesSuccessfully(t *testing.T) { // Test getting by namespace ID t.Run("get by namespace ID", func(t *testing.T) { req := handler.Request{ - NamespaceId: &namespaceID, + Namespace: namespaceID, } res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) @@ -109,7 +109,7 @@ func TestListOverridesSuccessfully(t *testing.T) { require.NoError(t, err) req := handler.Request{ - NamespaceId: &emptyNamespaceID, + Namespace: emptyNamespaceID, } res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) diff --git a/go/apps/api/routes/v2_ratelimit_list_overrides/400_test.go b/go/apps/api/routes/v2_ratelimit_list_overrides/400_test.go index 667b8593fd..258fbe59c7 100644 --- a/go/apps/api/routes/v2_ratelimit_list_overrides/400_test.go +++ b/go/apps/api/routes/v2_ratelimit_list_overrides/400_test.go @@ -38,7 +38,7 @@ func TestBadRequests(t *testing.T) { require.NotNil(t, res.Body) require.Equal(t, "https://unkey.com/docs/api-reference/errors-v2/unkey/application/invalid_input", res.Body.Error.Type) - require.Equal(t, "You must provide either a namespace ID or name.", res.Body.Error.Detail) + require.Equal(t, "POST request body for '/v2/ratelimit.listOverrides' failed to validate schema", res.Body.Error.Detail) require.Equal(t, http.StatusBadRequest, res.Body.Error.Status) require.Equal(t, "Bad Request", res.Body.Error.Title) require.NotEmpty(t, res.Body.Meta.RequestId) @@ -46,8 +46,7 @@ func TestBadRequests(t *testing.T) { t.Run("neither namespace ID nor name provided", func(t *testing.T) { req := openapi.V2RatelimitListOverridesRequestBody{ - NamespaceId: nil, - NamespaceName: nil, + Namespace: "", } res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse](h, route, headers, req) @@ -56,11 +55,11 @@ func TestBadRequests(t *testing.T) { require.NotNil(t, res.Body) require.Equal(t, "https://unkey.com/docs/api-reference/errors-v2/unkey/application/invalid_input", res.Body.Error.Type) - require.Equal(t, "You must provide either a namespace ID or name.", res.Body.Error.Detail) + require.Equal(t, "POST request body for '/v2/ratelimit.listOverrides' failed to validate schema", res.Body.Error.Detail) require.Equal(t, http.StatusBadRequest, res.Body.Error.Status) require.Equal(t, "Bad Request", res.Body.Error.Title) require.NotEmpty(t, res.Body.Meta.RequestId) - require.Equal(t, len(res.Body.Error.Errors), 0) + require.Greater(t, len(res.Body.Error.Errors), 0) }) t.Run("malformed authorization header", func(t *testing.T) { @@ -69,9 +68,8 @@ func TestBadRequests(t *testing.T) { "Authorization": {"malformed_header"}, } - namespaceName := "test_namespace" req := handler.Request{ - NamespaceName: &namespaceName, + Namespace: "test_namespace", } res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) @@ -85,9 +83,8 @@ func TestBadRequests(t *testing.T) { // No Authorization header } - namespaceName := "test_namespace" req := handler.Request{ - NamespaceName: &namespaceName, + Namespace: "test_namespace", } res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) diff --git a/go/apps/api/routes/v2_ratelimit_list_overrides/401_test.go b/go/apps/api/routes/v2_ratelimit_list_overrides/401_test.go index 9e4720c312..be024c5d7c 100644 --- a/go/apps/api/routes/v2_ratelimit_list_overrides/401_test.go +++ b/go/apps/api/routes/v2_ratelimit_list_overrides/401_test.go @@ -26,9 +26,8 @@ func TestUnauthorizedAccess(t *testing.T) { "Authorization": {"Bearer invalid_token"}, } - namespaceName := "test_namespace" req := handler.Request{ - NamespaceName: &namespaceName, + Namespace: "test_namespace", } res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) diff --git a/go/apps/api/routes/v2_ratelimit_list_overrides/403_test.go b/go/apps/api/routes/v2_ratelimit_list_overrides/403_test.go index 17577434da..a18f9eae0a 100644 --- a/go/apps/api/routes/v2_ratelimit_list_overrides/403_test.go +++ b/go/apps/api/routes/v2_ratelimit_list_overrides/403_test.go @@ -63,7 +63,7 @@ func TestWorkspacePermissions(t *testing.T) { // Try to access the override with a key from a different workspace req := handler.Request{ - NamespaceId: &namespaceID, + Namespace: namespaceID, } res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse](h, route, headers, req) diff --git a/go/apps/api/routes/v2_ratelimit_list_overrides/404_test.go b/go/apps/api/routes/v2_ratelimit_list_overrides/404_test.go index 70aed9829e..c070c78fad 100644 --- a/go/apps/api/routes/v2_ratelimit_list_overrides/404_test.go +++ b/go/apps/api/routes/v2_ratelimit_list_overrides/404_test.go @@ -48,7 +48,7 @@ func TestOverrideNotFound(t *testing.T) { t.Run("namespace id not found", func(t *testing.T) { nonExistentNamespaceId := "ns_nonexistent" req := handler.Request{ - NamespaceId: &nonExistentNamespaceId, + Namespace: nonExistentNamespaceId, } res := testutil.CallRoute[handler.Request, openapi.NotFoundErrorResponse](h, route, headers, req) @@ -61,7 +61,7 @@ func TestOverrideNotFound(t *testing.T) { t.Run("namespace name not found", func(t *testing.T) { nonExistentNamespaceName := "nonexistent_namespace" req := handler.Request{ - NamespaceName: &nonExistentNamespaceName, + Namespace: nonExistentNamespaceName, } res := testutil.CallRoute[handler.Request, openapi.NotFoundErrorResponse](h, route, headers, req) diff --git a/go/apps/api/routes/v2_ratelimit_list_overrides/handler.go b/go/apps/api/routes/v2_ratelimit_list_overrides/handler.go index 75b3019e19..91381bf5c1 100644 --- a/go/apps/api/routes/v2_ratelimit_list_overrides/handler.go +++ b/go/apps/api/routes/v2_ratelimit_list_overrides/handler.go @@ -47,18 +47,30 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { return err } - namespace, err := getNamespace(ctx, h, auth.AuthorizedWorkspaceID, req) - - if db.IsNotFound(err) { - return fault.New("namespace not found", - fault.Code(codes.Data.RatelimitNamespace.NotFound.URN()), - fault.Internal("namespace not found"), fault.Public("This namespace does not exist."), - ) - } + // Use the namespace field directly - it can be either name or ID + response, err := db.Query.FindRatelimitNamespace(ctx, h.DB.RO(), db.FindRatelimitNamespaceParams{ + WorkspaceID: auth.AuthorizedWorkspaceID, + Namespace: req.Namespace, + }) if err != nil { + if db.IsNotFound(err) { + return fault.New("namespace not found", + fault.Code(codes.Data.RatelimitNamespace.NotFound.URN()), + fault.Internal("namespace not found"), fault.Public("This namespace does not exist."), + ) + } return err } + namespace := db.RatelimitNamespace{ + ID: response.ID, + WorkspaceID: response.WorkspaceID, + Name: response.Name, + CreatedAtM: response.CreatedAtM, + UpdatedAtM: response.UpdatedAtM, + DeletedAtM: response.DeletedAtM, + } + if namespace.WorkspaceID != auth.AuthorizedWorkspaceID { return fault.New("namespace not found", fault.Code(codes.Data.RatelimitNamespace.NotFound.URN()), @@ -90,7 +102,8 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { if err != nil { return err } - response := Response{ + + responseBody := Response{ Meta: openapi.Meta{ RequestId: s.RequestID(), }, @@ -102,7 +115,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { } for i, override := range overrides { - response.Data[i] = openapi.RatelimitOverride{ + responseBody.Data[i] = openapi.RatelimitOverride{ OverrideId: override.ID, Duration: int64(override.Duration), Identifier: override.Identifier, @@ -111,29 +124,5 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { } } - return s.JSON(http.StatusOK, response) -} - -func getNamespace(ctx context.Context, h *Handler, workspaceID string, req Request) (db.RatelimitNamespace, error) { - - switch { - case req.NamespaceId != nil: - { - return db.Query.FindRatelimitNamespaceByID(ctx, h.DB.RO(), *req.NamespaceId) - - } - case req.NamespaceName != nil: - { - return db.Query.FindRatelimitNamespaceByName(ctx, h.DB.RO(), db.FindRatelimitNamespaceByNameParams{ - WorkspaceID: workspaceID, - Name: *req.NamespaceName, - }) - } - } - - return db.RatelimitNamespace{}, fault.New("missing namespace id or name", - fault.Code(codes.App.Validation.InvalidInput.URN()), - fault.Internal("missing namespace id or name"), fault.Public("You must provide either a namespace ID or name."), - ) - + return s.JSON(http.StatusOK, responseBody) } diff --git a/go/apps/api/routes/v2_ratelimit_set_override/200_test.go b/go/apps/api/routes/v2_ratelimit_set_override/200_test.go index 7e5e671f9c..ecf02152b6 100644 --- a/go/apps/api/routes/v2_ratelimit_set_override/200_test.go +++ b/go/apps/api/routes/v2_ratelimit_set_override/200_test.go @@ -49,10 +49,10 @@ func TestSetOverrideSuccessfully(t *testing.T) { // Create a new override by namespace name t.Run("create override using namespace name", func(t *testing.T) { req := handler.Request{ - NamespaceName: &namespaceName, - Identifier: "user_123", - Limit: 10, - Duration: 1000, + Namespace: namespaceName, + Identifier: "user_123", + Limit: 10, + Duration: 1000, } res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) @@ -75,10 +75,10 @@ func TestSetOverrideSuccessfully(t *testing.T) { // Create a new override by namespace ID t.Run("create override using namespace ID", func(t *testing.T) { req := handler.Request{ - NamespaceId: &namespaceID, - Identifier: "user_456", - Limit: 20, - Duration: 2000, + Namespace: namespaceID, + Identifier: "user_456", + Limit: 20, + Duration: 2000, } res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) @@ -101,10 +101,10 @@ func TestSetOverrideSuccessfully(t *testing.T) { // Create an override with a wildcard identifier t.Run("create override with wildcard identifier", func(t *testing.T) { req := handler.Request{ - NamespaceId: &namespaceID, - Identifier: "*", // Wildcard - Limit: 5, - Duration: 2000, + Namespace: namespaceID, + Identifier: "*", // Wildcard + Limit: 5, + Duration: 2000, } res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) diff --git a/go/apps/api/routes/v2_ratelimit_set_override/400_test.go b/go/apps/api/routes/v2_ratelimit_set_override/400_test.go index 1023b3dc24..2879aa3fed 100644 --- a/go/apps/api/routes/v2_ratelimit_set_override/400_test.go +++ b/go/apps/api/routes/v2_ratelimit_set_override/400_test.go @@ -8,7 +8,6 @@ import ( "github.com/stretchr/testify/require" "github.com/unkeyed/unkey/go/apps/api/openapi" handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_ratelimit_set_override" - "github.com/unkeyed/unkey/go/pkg/ptr" "github.com/unkeyed/unkey/go/pkg/testutil" "github.com/unkeyed/unkey/go/pkg/uid" ) @@ -49,9 +48,9 @@ func TestBadRequests(t *testing.T) { t.Run("missing identifier", func(t *testing.T) { req := openapi.V2RatelimitSetOverrideRequestBody{ - NamespaceId: ptr.P("test_namespace_id"), - Limit: 10, - Duration: 1000, + Namespace: "test_namespace_id", + Limit: 10, + Duration: 1000, } res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse](h, route, headers, req) @@ -69,10 +68,10 @@ func TestBadRequests(t *testing.T) { t.Run("empty identifier", func(t *testing.T) { req := openapi.V2RatelimitSetOverrideRequestBody{ - NamespaceId: ptr.P("test_namespace_id"), - Identifier: "", - Limit: 10, - Duration: 1000, + Namespace: "test_namespace_id", + Identifier: "", + Limit: 10, + Duration: 1000, } res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse](h, route, headers, req) @@ -91,9 +90,9 @@ func TestBadRequests(t *testing.T) { t.Run("missing duration", func(t *testing.T) { req := openapi.V2RatelimitSetOverrideRequestBody{ - NamespaceId: ptr.P("test_namespace_id"), - Identifier: "user_123", - Limit: 10, + Namespace: "test_namespace_id", + Identifier: "user_123", + Limit: 10, } res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse](h, route, headers, req) @@ -111,10 +110,10 @@ func TestBadRequests(t *testing.T) { t.Run("invalid limit (negative)", func(t *testing.T) { req := openapi.V2RatelimitSetOverrideRequestBody{ - NamespaceId: ptr.P("test_namespace_id"), - Identifier: "user_123", - Limit: -10, - Duration: 1000, + Namespace: "test_namespace_id", + Identifier: "user_123", + Limit: -10, + Duration: 1000, } res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse](h, route, headers, req) @@ -132,10 +131,10 @@ func TestBadRequests(t *testing.T) { t.Run("invalid duration (negative)", func(t *testing.T) { req := openapi.V2RatelimitSetOverrideRequestBody{ - NamespaceId: ptr.P("test_namespace_id"), - Identifier: "user_123", - Limit: 10, - Duration: -1000, + Namespace: "test_namespace_id", + Identifier: "user_123", + Limit: 10, + Duration: -1000, } res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse](h, route, headers, req) @@ -153,11 +152,10 @@ func TestBadRequests(t *testing.T) { t.Run("neither namespace ID nor name provided", func(t *testing.T) { req := openapi.V2RatelimitSetOverrideRequestBody{ - NamespaceId: nil, - NamespaceName: nil, - Identifier: "user_123", - Limit: 10, - Duration: 1000, + Namespace: "", + Identifier: "user_123", + Limit: 10, + Duration: 1000, } res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse](h, route, headers, req) @@ -166,11 +164,11 @@ func TestBadRequests(t *testing.T) { require.NotNil(t, res.Body) require.Equal(t, "https://unkey.com/docs/api-reference/errors-v2/unkey/application/invalid_input", res.Body.Error.Type) - require.Equal(t, "You must provide either a namespace ID or name.", res.Body.Error.Detail) + require.Equal(t, "POST request body for '/v2/ratelimit.setOverride' failed to validate schema", res.Body.Error.Detail) require.Equal(t, http.StatusBadRequest, res.Body.Error.Status) require.Equal(t, "Bad Request", res.Body.Error.Title) require.NotEmpty(t, res.Body.Meta.RequestId) - require.Equal(t, len(res.Body.Error.Errors), 0) + require.Greater(t, len(res.Body.Error.Errors), 0) }) t.Run("malformed authorization header", func(t *testing.T) { @@ -179,12 +177,11 @@ func TestBadRequests(t *testing.T) { "Authorization": {"malformed_header"}, } - namespaceName := uid.New("test") req := handler.Request{ - NamespaceName: &namespaceName, - Identifier: "test_identifier", - Limit: 10, - Duration: 1000, + Namespace: uid.New("test"), + Identifier: "test_identifier", + Limit: 10, + Duration: 1000, } res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) diff --git a/go/apps/api/routes/v2_ratelimit_set_override/401_test.go b/go/apps/api/routes/v2_ratelimit_set_override/401_test.go index ab6e131c8c..dcbd204fe6 100644 --- a/go/apps/api/routes/v2_ratelimit_set_override/401_test.go +++ b/go/apps/api/routes/v2_ratelimit_set_override/401_test.go @@ -29,12 +29,11 @@ func TestUnauthorizedAccess(t *testing.T) { "Authorization": {"Bearer invalid_token"}, } - namespaceName := uid.New("test") req := handler.Request{ - NamespaceName: &namespaceName, - Identifier: "test_identifier", - Limit: 10, - Duration: 1000, + Namespace: uid.New("test"), + Identifier: "test_identifier", + Limit: 10, + Duration: 1000, } res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) diff --git a/go/apps/api/routes/v2_ratelimit_set_override/403_test.go b/go/apps/api/routes/v2_ratelimit_set_override/403_test.go index 9c02ccc4b3..fc49558b45 100644 --- a/go/apps/api/routes/v2_ratelimit_set_override/403_test.go +++ b/go/apps/api/routes/v2_ratelimit_set_override/403_test.go @@ -52,10 +52,10 @@ func TestWorkspacePermissions(t *testing.T) { // Try to create an override using a namespace from the default workspace // but with a key from a different workspace req := handler.Request{ - NamespaceId: &namespaceID, - Identifier: "test_identifier", - Limit: 10, - Duration: 1000, + Namespace: namespaceID, + Identifier: "test_identifier", + Limit: 10, + Duration: 1000, } res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse](h, route, headers, req) diff --git a/go/apps/api/routes/v2_ratelimit_set_override/404_test.go b/go/apps/api/routes/v2_ratelimit_set_override/404_test.go index 2458fe82d0..cc70747b49 100644 --- a/go/apps/api/routes/v2_ratelimit_set_override/404_test.go +++ b/go/apps/api/routes/v2_ratelimit_set_override/404_test.go @@ -33,12 +33,11 @@ func TestNamespaceNotFound(t *testing.T) { // Test with non-existent namespace ID t.Run("namespace id not found", func(t *testing.T) { - nonExistentNamespaceId := "ns_nonexistent" req := handler.Request{ - NamespaceId: &nonExistentNamespaceId, - Identifier: "some_identifier", - Limit: 10, - Duration: 1000, + Namespace: "ns_nonexistent", + Identifier: "some_identifier", + Limit: 10, + Duration: 1000, } res := testutil.CallRoute[handler.Request, openapi.NotFoundErrorResponse](h, route, headers, req) @@ -49,12 +48,11 @@ func TestNamespaceNotFound(t *testing.T) { // Test with non-existent namespace name t.Run("namespace name not found", func(t *testing.T) { - nonExistentNamespaceName := "nonexistent_namespace" req := handler.Request{ - NamespaceName: &nonExistentNamespaceName, - Identifier: "some_identifier", - Limit: 10, - Duration: 1000, + Namespace: "nonexistent_namespace", + Identifier: "some_identifier", + Limit: 10, + Duration: 1000, } res := testutil.CallRoute[handler.Request, openapi.NotFoundErrorResponse](h, route, headers, req) diff --git a/go/apps/api/routes/v2_ratelimit_set_override/handler.go b/go/apps/api/routes/v2_ratelimit_set_override/handler.go index e8185a4cf0..0faeefadd7 100644 --- a/go/apps/api/routes/v2_ratelimit_set_override/handler.go +++ b/go/apps/api/routes/v2_ratelimit_set_override/handler.go @@ -2,8 +2,6 @@ package handler import ( "context" - "database/sql" - "errors" "fmt" "net/http" "time" @@ -57,10 +55,14 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { return err } - namespace, err := getNamespace(ctx, h, auth.AuthorizedWorkspaceID, req) + // Use the namespace field directly - it can be either name or ID + response, err := db.Query.FindRatelimitNamespace(ctx, h.DB.RO(), db.FindRatelimitNamespaceParams{ + WorkspaceID: auth.AuthorizedWorkspaceID, + Namespace: req.Namespace, + }) if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return fault.Wrap(err, + if db.IsNotFound(err) { + return fault.New("namespace not found", fault.Code(codes.Data.RatelimitNamespace.NotFound.URN()), fault.Internal("namespace not found"), fault.Public("This namespace does not exist."), ) @@ -68,6 +70,15 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { return err } + namespace := db.RatelimitNamespace{ + ID: response.ID, + WorkspaceID: response.WorkspaceID, + Name: response.Name, + CreatedAtM: response.CreatedAtM, + UpdatedAtM: response.UpdatedAtM, + DeletedAtM: response.DeletedAtM, + } + if namespace.WorkspaceID != auth.AuthorizedWorkspaceID { return fault.New("namespace not found", fault.Code(codes.Data.RatelimitNamespace.NotFound.URN()), @@ -163,26 +174,3 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { }, }) } - -func getNamespace(ctx context.Context, h *Handler, workspaceID string, req Request) (db.RatelimitNamespace, error) { - - switch { - case req.NamespaceId != nil: - { - return db.Query.FindRatelimitNamespaceByID(ctx, h.DB.RO(), *req.NamespaceId) - } - case req.NamespaceName != nil: - { - return db.Query.FindRatelimitNamespaceByName(ctx, h.DB.RO(), db.FindRatelimitNamespaceByNameParams{ - WorkspaceID: workspaceID, - Name: *req.NamespaceName, - }) - } - } - - return db.RatelimitNamespace{}, fault.New("missing namespace id or name", - fault.Code(codes.App.Validation.InvalidInput.URN()), - fault.Internal("missing namespace id or name"), fault.Public("You must provide either a namespace ID or name."), - ) - -} diff --git a/go/pkg/db/querier_generated.go b/go/pkg/db/querier_generated.go index e1da2ba651..55d36bf2f1 100644 --- a/go/pkg/db/querier_generated.go +++ b/go/pkg/db/querier_generated.go @@ -381,9 +381,7 @@ type Querier interface { // ) as overrides // FROM `ratelimit_namespaces` ns // WHERE ns.workspace_id = ? - // AND CASE WHEN ? IS NOT NULL THEN ns.name = ? - // WHEN ? IS NOT NULL THEN ns.id = ? - // ELSE false END + // AND (ns.id = ? OR ns.name = ?) FindRatelimitNamespace(ctx context.Context, db DBTX, arg FindRatelimitNamespaceParams) (FindRatelimitNamespaceRow, error) //FindRatelimitNamespaceByID // diff --git a/go/pkg/db/queries/ratelimit_namespace_find.sql b/go/pkg/db/queries/ratelimit_namespace_find.sql index dbd9e8394e..6e9220f749 100644 --- a/go/pkg/db/queries/ratelimit_namespace_find.sql +++ b/go/pkg/db/queries/ratelimit_namespace_find.sql @@ -13,7 +13,5 @@ SELECT *, json_array() ) as overrides FROM `ratelimit_namespaces` ns -WHERE ns.workspace_id = ? -AND CASE WHEN sqlc.narg('name') IS NOT NULL THEN ns.name = sqlc.narg('name') -WHEN sqlc.narg('id') IS NOT NULL THEN ns.id = sqlc.narg('id') -ELSE false END; +WHERE ns.workspace_id = sqlc.arg(workspace_id) +AND (ns.id = sqlc.arg(namespace) OR ns.name = sqlc.arg(namespace)); diff --git a/go/pkg/db/ratelimit_namespace_find.sql_generated.go b/go/pkg/db/ratelimit_namespace_find.sql_generated.go index e054ec25af..041c0d8be0 100644 --- a/go/pkg/db/ratelimit_namespace_find.sql_generated.go +++ b/go/pkg/db/ratelimit_namespace_find.sql_generated.go @@ -25,16 +25,13 @@ SELECT id, workspace_id, name, created_at_m, updated_at_m, deleted_at_m, json_array() ) as overrides FROM ` + "`" + `ratelimit_namespaces` + "`" + ` ns -WHERE ns.workspace_id = ? -AND CASE WHEN ? IS NOT NULL THEN ns.name = ? -WHEN ? IS NOT NULL THEN ns.id = ? -ELSE false END +WHERE ns.workspace_id = ? +AND (ns.id = ? OR ns.name = ?) ` type FindRatelimitNamespaceParams struct { - WorkspaceID string `db:"workspace_id"` - Name sql.NullString `db:"name"` - ID sql.NullString `db:"id"` + WorkspaceID string `db:"workspace_id"` + Namespace string `db:"namespace"` } type FindRatelimitNamespaceRow struct { @@ -64,17 +61,9 @@ type FindRatelimitNamespaceRow struct { // ) as overrides // FROM `ratelimit_namespaces` ns // WHERE ns.workspace_id = ? -// AND CASE WHEN ? IS NOT NULL THEN ns.name = ? -// WHEN ? IS NOT NULL THEN ns.id = ? -// ELSE false END +// AND (ns.id = ? OR ns.name = ?) func (q *Queries) FindRatelimitNamespace(ctx context.Context, db DBTX, arg FindRatelimitNamespaceParams) (FindRatelimitNamespaceRow, error) { - row := db.QueryRowContext(ctx, findRatelimitNamespace, - arg.WorkspaceID, - arg.Name, - arg.Name, - arg.ID, - arg.ID, - ) + row := db.QueryRowContext(ctx, findRatelimitNamespace, arg.WorkspaceID, arg.Namespace, arg.Namespace) var i FindRatelimitNamespaceRow err := row.Scan( &i.ID, From 591774d6dff9b464857058f4d98926ee06399716 Mon Sep 17 00:00:00 2001 From: chronark Date: Thu, 24 Jul 2025 17:32:26 +0200 Subject: [PATCH 3/9] feat: accept both namespace name or id --- go/apps/api/openapi/gen.go | 40 ++------- go/apps/api/openapi/openapi-generated.yaml | 53 ++++-------- .../V2RatelimitDeleteOverrideRequestBody.yaml | 17 +--- .../V2RatelimitGetOverrideRequestBody.yaml | 22 +---- .../limit/V2RatelimitLimitRequestBody.yaml | 7 +- .../V2RatelimitListOverridesRequestBody.yaml | 16 ++-- .../V2RatelimitSetOverrideRequestBody.yaml | 13 +-- .../v2_ratelimit_delete_override/400_test.go | 2 +- .../v2_ratelimit_delete_override/handler.go | 78 +++++++---------- .../v2_ratelimit_get_override/200_test.go | 2 +- .../v2_ratelimit_get_override/handler.go | 84 +++++++++++-------- .../api/routes/v2_ratelimit_limit/handler.go | 2 +- .../v2_ratelimit_list_overrides/400_test.go | 4 +- .../v2_ratelimit_set_override/200_test.go | 16 ++-- .../v2_ratelimit_set_override/400_test.go | 2 +- .../v2_ratelimit_set_override/handler.go | 82 ++++++++---------- 16 files changed, 164 insertions(+), 276 deletions(-) diff --git a/go/apps/api/openapi/gen.go b/go/apps/api/openapi/gen.go index 6f7c49a30b..d5b3413eac 100644 --- a/go/apps/api/openapi/gen.go +++ b/go/apps/api/openapi/gen.go @@ -1823,13 +1823,7 @@ type V2RatelimitDeleteOverrideRequestBody struct { // After deletion, any identifiers previously affected by this override will immediately revert to using the default rate limit for the namespace. Identifier string `json:"identifier"` - // Namespace The rate limit namespace identifier. This can be either: - // - A namespace name (e.g., "api.requests", "auth.login") - human-readable names that are unique within your workspace - // - A namespace ID (e.g., "ns_1234567890abcdef") - system-generated unique identifiers - // - // The system will automatically detect whether you've provided a name or ID and perform the appropriate lookup. - // Names must start with a letter and can contain letters, numbers, underscores, dots, slashes, or hyphens. - // IDs follow the format "ns_" followed by an alphanumeric string. + // Namespace The id or name of the namespace containing the override. Namespace string `json:"namespace"` } @@ -1864,13 +1858,7 @@ type V2RatelimitGetOverrideRequestBody struct { // This field is used to look up the specific override configuration for this pattern. Identifier string `json:"identifier"` - // Namespace The rate limit namespace identifier. This can be either: - // - A namespace name (e.g., "api.requests", "auth.login") - human-readable names that are unique within your workspace - // - A namespace ID (e.g., "ns_1234567890abcdef") - system-generated unique identifiers - // - // The system will automatically detect whether you've provided a name or ID and perform the appropriate lookup. - // Names must start with a letter and can contain letters, numbers, underscores, dots, slashes, or hyphens. - // IDs follow the format "ns_" followed by an alphanumeric string. + // Namespace The id or name of the namespace containing the override. Namespace string `json:"namespace"` } @@ -1909,13 +1897,7 @@ type V2RatelimitLimitRequestBody struct { // Consider system capacity, business requirements, and fair usage policies in limit determination. Limit int64 `json:"limit"` - // Namespace The rate limit namespace identifier. This can be either: - // - A namespace name (e.g., "api.requests", "auth.login") - human-readable names that are unique within your workspace - // - A namespace ID (e.g., "ns_1234567890abcdef") - system-generated unique identifiers - // - // The system will automatically detect whether you've provided a name or ID and perform the appropriate lookup. - // Names must start with a letter and can contain letters, numbers, underscores, dots, slashes, or hyphens. - // IDs follow the format "ns_" followed by an alphanumeric string. + // Namespace The id or name of the namespace. Namespace string `json:"namespace"` } @@ -1983,13 +1965,7 @@ type V2RatelimitListOverridesRequestBody struct { // Results exceeding this limit will be paginated, with a cursor provided for fetching subsequent pages. Limit *int `json:"limit,omitempty"` - // Namespace The rate limit namespace identifier. This can be either: - // - A namespace name (e.g., "api.requests", "auth.login") - human-readable names that are unique within your workspace - // - A namespace ID (e.g., "ns_1234567890abcdef") - system-generated unique identifiers - // - // The system will automatically detect whether you've provided a name or ID and perform the appropriate lookup. - // Names must start with a letter and can contain letters, numbers, underscores, dots, slashes, or hyphens. - // IDs follow the format "ns_" followed by an alphanumeric string. + // Namespace The id or name of the rate limit namespace to list overrides for. Namespace string `json:"namespace"` } @@ -2050,13 +2026,7 @@ type V2RatelimitSetOverrideRequestBody struct { // This limit entirely replaces the default limit for matching identifiers. Limit int64 `json:"limit"` - // Namespace The rate limit namespace identifier. This can be either: - // - A namespace name (e.g., "api.requests", "auth.login") - human-readable names that are unique within your workspace - // - A namespace ID (e.g., "ns_1234567890abcdef") - system-generated unique identifiers - // - // The system will automatically detect whether you've provided a name or ID and perform the appropriate lookup. - // Names must start with a letter and can contain letters, numbers, underscores, dots, slashes, or hyphens. - // IDs follow the format "ns_" followed by an alphanumeric string. + // Namespace The ID or name of the rate limit namespace. Namespace string `json:"namespace"` } diff --git a/go/apps/api/openapi/openapi-generated.yaml b/go/apps/api/openapi/openapi-generated.yaml index 74c7638c0f..af4e35ee46 100644 --- a/go/apps/api/openapi/openapi-generated.yaml +++ b/go/apps/api/openapi/openapi-generated.yaml @@ -1,5 +1,5 @@ # Code generated by generate_bundle.go; DO NOT EDIT. -# Generated at: 2025-07-24T13:37:52Z +# Generated at: 2025-07-24T15:18:23Z # Source: openapi-split.yaml components: @@ -1788,13 +1788,8 @@ components: Once deleted, the override cannot be recovered, and the operation takes effect immediately. additionalProperties: false properties: - namespaceId: - description: The unique ID of the rate limit namespace containing the override. Either `namespaceId` or `namespaceName` must be provided, but not both. Using `namespaceId` is more precise and less prone to naming conflicts, making it ideal for automation and scripts. - type: string - minLength: 1 - maxLength: 255 - namespaceName: - description: The name of the rate limit namespace containing the override. Either `namespaceId` or `namespaceName` must be provided, but not both. Using `namespaceName` is more human-readable and convenient for manual operations and configurations. + namespace: + description: The id or name of the namespace containing the override. type: string minLength: 1 maxLength: 255 @@ -1812,6 +1807,7 @@ components: minLength: 1 maxLength: 255 required: + - namespace - identifier type: object V2RatelimitDeleteOverrideResponseBody: @@ -1836,13 +1832,8 @@ components: - Retrieving override settings for modification additionalProperties: false properties: - namespaceId: - description: The unique ID of the rate limit namespace. Either `namespaceId` or `namespaceName` must be provided, but not both. Using `namespaceId` is more precise and less prone to naming conflicts, making it ideal for scripts and automated operations. - type: string - minLength: 1 - maxLength: 255 - namespaceName: - description: The name of the rate limit namespace. Either `namespaceId` or `namespaceName` must be provided, but not both. Using `namespaceName` is more human-readable and easier to work with for manual operations and configurations. + namespace: + description: The id or name of the namespace containing the override. type: string minLength: 1 maxLength: 255 @@ -1860,15 +1851,9 @@ components: minLength: 1 maxLength: 255 required: + - namespace - identifier type: object - oneOf: - - required: - - namespaceName - - identifier - - required: - - namespaceId - - identifier V2RatelimitGetOverrideResponseBody: type: object required: @@ -1887,12 +1872,7 @@ components: minLength: 1 maxLength: 255 pattern: "^[a-zA-Z][a-zA-Z0-9_./-]*$" - description: | - Identifies the rate limit category using hierarchical naming for organization and monitoring. - Namespaces must start with a letter and can contain letters, numbers, underscores, dots, slashes, or hyphens. - Use descriptive, hierarchical names like 'auth.login', 'api.requests', or 'media.uploads' for clear categorization. - Namespaces must be unique within your workspace and support segmentation of different API operations. - Consistent naming conventions across your application improve monitoring and debugging capabilities. + description: The id or name of the namespace. example: sms.sign_up cost: type: integer @@ -1959,14 +1939,11 @@ components: V2RatelimitListOverridesRequestBody: additionalProperties: false properties: - namespaceId: - description: The unique ID of the rate limit namespace to list overrides for. Either `namespaceId` or `namespaceName` must be provided, but not both. Using `namespaceId` guarantees you're targeting the exact namespace intended, even if names change over time. + namespace: + description: The id or name of the rate limit namespace to list overrides for. type: string minLength: 1 maxLength: 255 - namespaceName: - description: The name of the rate limit namespace to list overrides for. Either `namespaceId` or `namespaceName` must be provided, but not both. Using `namespaceName` is more human-readable and convenient for manual operations and dashboards. - type: string cursor: description: Pagination cursor from a previous response. Include this when fetching subsequent pages of results. Each response containing more results than the requested limit will include a cursor value in the pagination object that can be used here. type: string @@ -1983,6 +1960,8 @@ components: default: 10 minimum: 1 maximum: 100 + required: + - namespace type: object V2RatelimitListOverridesResponseBody: type: object @@ -2008,14 +1987,11 @@ components: - Prioritizing important clients with higher limits additionalProperties: false properties: - namespaceId: - description: The unique ID of the rate limit namespace. Either `namespaceId` or `namespaceName` must be provided, but not both. Using `namespaceId` guarantees you're targeting the exact namespace intended, even if names change, making it ideal for automation and scripts. + namespace: + description: The ID or name of the rate limit namespace. type: string minLength: 1 maxLength: 255 - namespaceName: - description: The name of the rate limit namespace. Either `namespaceId` or `namespaceName` must be provided, but not both. Using `namespaceName` is more human-readable and convenient for manual operations and configurations. - type: string duration: description: |- The duration in milliseconds for the rate limit window. This defines how long the rate limit counter accumulates before resetting to zero. @@ -2060,6 +2036,7 @@ components: type: integer minimum: 0 required: + - namespace - identifier - limit - duration diff --git a/go/apps/api/openapi/spec/paths/v2/ratelimit/deleteOverride/V2RatelimitDeleteOverrideRequestBody.yaml b/go/apps/api/openapi/spec/paths/v2/ratelimit/deleteOverride/V2RatelimitDeleteOverrideRequestBody.yaml index 82773f97b8..ef3e56e720 100644 --- a/go/apps/api/openapi/spec/paths/v2/ratelimit/deleteOverride/V2RatelimitDeleteOverrideRequestBody.yaml +++ b/go/apps/api/openapi/spec/paths/v2/ratelimit/deleteOverride/V2RatelimitDeleteOverrideRequestBody.yaml @@ -11,20 +11,8 @@ description: |- Once deleted, the override cannot be recovered, and the operation takes effect immediately. additionalProperties: false properties: - namespaceId: - description: - The unique ID of the rate limit namespace containing the override. - Either `namespaceId` or `namespaceName` must be provided, but not both. Using - `namespaceId` is more precise and less prone to naming conflicts, making - it ideal for automation and scripts. - type: string - minLength: 1 - maxLength: 255 - namespaceName: - description: The name of the rate limit namespace containing the override. - Either `namespaceId` or `namespaceName` must be provided, but not both. Using - `namespaceName` is more human-readable and convenient for manual operations - and configurations. + namespace: + description: The id or name of the namespace containing the override. type: string minLength: 1 maxLength: 255 @@ -42,5 +30,6 @@ properties: minLength: 1 maxLength: 255 required: + - namespace - identifier type: object diff --git a/go/apps/api/openapi/spec/paths/v2/ratelimit/getOverride/V2RatelimitGetOverrideRequestBody.yaml b/go/apps/api/openapi/spec/paths/v2/ratelimit/getOverride/V2RatelimitGetOverrideRequestBody.yaml index 1d2a36d3a6..174cff4c83 100644 --- a/go/apps/api/openapi/spec/paths/v2/ratelimit/getOverride/V2RatelimitGetOverrideRequestBody.yaml +++ b/go/apps/api/openapi/spec/paths/v2/ratelimit/getOverride/V2RatelimitGetOverrideRequestBody.yaml @@ -9,18 +9,8 @@ description: |- - Retrieving override settings for modification additionalProperties: false properties: - namespaceId: - description: The unique ID of the rate limit namespace. Either `namespaceId` - or `namespaceName` must be provided, but not both. Using `namespaceId` is - more precise and less prone to naming conflicts, making it ideal for scripts - and automated operations. - type: string - minLength: 1 - maxLength: 255 - namespaceName: - description: The name of the rate limit namespace. Either `namespaceId` or - `namespaceName` must be provided, but not both. Using `namespaceName` is more - human-readable and easier to work with for manual operations and configurations. + namespace: + description: The id or name of the namespace containing the override. type: string minLength: 1 maxLength: 255 @@ -38,12 +28,6 @@ properties: minLength: 1 maxLength: 255 required: + - namespace - identifier type: object -oneOf: - - required: - - namespaceName - - identifier - - required: - - namespaceId - - identifier diff --git a/go/apps/api/openapi/spec/paths/v2/ratelimit/limit/V2RatelimitLimitRequestBody.yaml b/go/apps/api/openapi/spec/paths/v2/ratelimit/limit/V2RatelimitLimitRequestBody.yaml index 26e2c758d9..3243008f73 100644 --- a/go/apps/api/openapi/spec/paths/v2/ratelimit/limit/V2RatelimitLimitRequestBody.yaml +++ b/go/apps/api/openapi/spec/paths/v2/ratelimit/limit/V2RatelimitLimitRequestBody.yaml @@ -5,12 +5,7 @@ properties: minLength: 1 maxLength: 255 # Reasonable upper bound for namespace identifiers pattern: "^[a-zA-Z][a-zA-Z0-9_./-]*$" - description: | - Identifies the rate limit category using hierarchical naming for organization and monitoring. - Namespaces must start with a letter and can contain letters, numbers, underscores, dots, slashes, or hyphens. - Use descriptive, hierarchical names like 'auth.login', 'api.requests', or 'media.uploads' for clear categorization. - Namespaces must be unique within your workspace and support segmentation of different API operations. - Consistent naming conventions across your application improve monitoring and debugging capabilities. + description: The id or name of the namespace. example: sms.sign_up cost: type: integer diff --git a/go/apps/api/openapi/spec/paths/v2/ratelimit/listOverrides/V2RatelimitListOverridesRequestBody.yaml b/go/apps/api/openapi/spec/paths/v2/ratelimit/listOverrides/V2RatelimitListOverridesRequestBody.yaml index b4a50441d7..353e745e9b 100644 --- a/go/apps/api/openapi/spec/paths/v2/ratelimit/listOverrides/V2RatelimitListOverridesRequestBody.yaml +++ b/go/apps/api/openapi/spec/paths/v2/ratelimit/listOverrides/V2RatelimitListOverridesRequestBody.yaml @@ -1,19 +1,11 @@ additionalProperties: false properties: - namespaceId: - description: The unique ID of the rate limit namespace to list overrides - for. Either `namespaceId` or `namespaceName` must be provided, but not both. - Using `namespaceId` guarantees you're targeting the exact namespace intended, - even if names change over time. + namespace: + description: The id or name of the rate limit namespace to list overrides + for. type: string minLength: 1 maxLength: 255 - namespaceName: - description: The name of the rate limit namespace to list overrides for. - Either `namespaceId` or `namespaceName` must be provided, but not both. Using - `namespaceName` is more human-readable and convenient for manual operations - and dashboards. - type: string cursor: description: Pagination cursor from a previous response. Include this when fetching subsequent pages of results. Each response containing more results @@ -33,4 +25,6 @@ properties: default: 10 minimum: 1 maximum: 100 +required: + - namespace type: object diff --git a/go/apps/api/openapi/spec/paths/v2/ratelimit/setOverride/V2RatelimitSetOverrideRequestBody.yaml b/go/apps/api/openapi/spec/paths/v2/ratelimit/setOverride/V2RatelimitSetOverrideRequestBody.yaml index 3c1ada9a84..10a5bf17ee 100644 --- a/go/apps/api/openapi/spec/paths/v2/ratelimit/setOverride/V2RatelimitSetOverrideRequestBody.yaml +++ b/go/apps/api/openapi/spec/paths/v2/ratelimit/setOverride/V2RatelimitSetOverrideRequestBody.yaml @@ -9,19 +9,11 @@ description: |- - Prioritizing important clients with higher limits additionalProperties: false properties: - namespaceId: - description: The unique ID of the rate limit namespace. Either `namespaceId` - or `namespaceName` must be provided, but not both. Using `namespaceId` guarantees - you're targeting the exact namespace intended, even if names change, making - it ideal for automation and scripts. + namespace: + description: The ID or name of the rate limit namespace. type: string minLength: 1 maxLength: 255 - namespaceName: - description: The name of the rate limit namespace. Either `namespaceId` or - `namespaceName` must be provided, but not both. Using `namespaceName` is more - human-readable and convenient for manual operations and configurations. - type: string duration: description: |- The duration in milliseconds for the rate limit window. This defines how long the rate limit counter accumulates before resetting to zero. @@ -66,6 +58,7 @@ properties: type: integer minimum: 0 required: + - namespace - identifier - limit - duration diff --git a/go/apps/api/routes/v2_ratelimit_delete_override/400_test.go b/go/apps/api/routes/v2_ratelimit_delete_override/400_test.go index e57f7021f8..fe1d02ae9e 100644 --- a/go/apps/api/routes/v2_ratelimit_delete_override/400_test.go +++ b/go/apps/api/routes/v2_ratelimit_delete_override/400_test.go @@ -95,7 +95,7 @@ func TestBadRequests(t *testing.T) { require.NotNil(t, res.Body) require.Equal(t, "https://unkey.com/docs/errors/unkey/application/invalid_input", res.Body.Error.Type) - require.Equal(t, "You must provide either a namespace ID or name.", res.Body.Error.Detail) + require.Equal(t, "POST request body for '/v2/ratelimit.deleteOverride' failed to validate schema", res.Body.Error.Detail) require.Equal(t, http.StatusBadRequest, res.Body.Error.Status) require.Equal(t, "Bad Request", res.Body.Error.Title) require.NotEmpty(t, res.Body.Meta.RequestId) diff --git a/go/apps/api/routes/v2_ratelimit_delete_override/handler.go b/go/apps/api/routes/v2_ratelimit_delete_override/handler.go index 8cde4cab04..c678d5297a 100644 --- a/go/apps/api/routes/v2_ratelimit_delete_override/handler.go +++ b/go/apps/api/routes/v2_ratelimit_delete_override/handler.go @@ -54,58 +54,44 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { return err } - // Use the namespace field directly - it can be either name or ID - namespaceKey := req.Namespace + err = db.Tx(ctx, h.DB.RW(), func(ctx context.Context, tx db.DBTX) error { + namespace, err := db.Query.FindRatelimitNamespace(ctx, tx, db.FindRatelimitNamespaceParams{ + WorkspaceID: auth.AuthorizedWorkspaceID, + Namespace: req.Namespace, + }) + if err != nil { + if db.IsNotFound(err) { + return fault.New("namespace not found", + fault.Code(codes.Data.RatelimitNamespace.NotFound.URN()), + fault.Internal("namespace not found"), + fault.Public("This namespace does not exist."), + ) + } + return err + } - response, err := db.Query.FindRatelimitNamespace(ctx, h.DB.RO(), db.FindRatelimitNamespaceParams{ - WorkspaceID: auth.AuthorizedWorkspaceID, - Namespace: namespaceKey, - }) - if err != nil { - if db.IsNotFound(err) { - return fault.New("namespace not found", + if namespace.DeletedAtM.Valid { + return fault.New("namespace was deleted", fault.Code(codes.Data.RatelimitNamespace.NotFound.URN()), - fault.Internal("namespace not found"), fault.Public("This namespace does not exist."), ) } - return err - } - - namespace := db.RatelimitNamespace{ - ID: response.ID, - WorkspaceID: response.WorkspaceID, - Name: response.Name, - CreatedAtM: response.CreatedAtM, - UpdatedAtM: response.UpdatedAtM, - DeletedAtM: response.DeletedAtM, - } - if namespace.WorkspaceID != auth.AuthorizedWorkspaceID { - return fault.New("namespace not found", - fault.Code(codes.Data.RatelimitNamespace.NotFound.URN()), - fault.Internal("wrong workspace, masking as 404"), - fault.Public("This namespace does not exist."), - ) - } - - err = auth.Verify(ctx, keys.WithPermissions(rbac.Or( - rbac.T(rbac.Tuple{ - ResourceType: rbac.Ratelimit, - ResourceID: namespace.ID, - Action: rbac.DeleteOverride, - }), - rbac.T(rbac.Tuple{ - ResourceType: rbac.Ratelimit, - ResourceID: "*", - Action: rbac.DeleteOverride, - }), - ))) - if err != nil { - return err - } - - err = db.Tx(ctx, h.DB.RW(), func(ctx context.Context, tx db.DBTX) error { + err = auth.Verify(ctx, keys.WithPermissions(rbac.Or( + rbac.T(rbac.Tuple{ + ResourceType: rbac.Ratelimit, + ResourceID: namespace.ID, + Action: rbac.DeleteOverride, + }), + rbac.T(rbac.Tuple{ + ResourceType: rbac.Ratelimit, + ResourceID: "*", + Action: rbac.DeleteOverride, + }), + ))) + if err != nil { + return err + } // Check if the override exists before deleting override, overrideErr := db.Query.FindRatelimitOverrideByIdentifier(ctx, tx, db.FindRatelimitOverrideByIdentifierParams{ WorkspaceID: auth.AuthorizedWorkspaceID, diff --git a/go/apps/api/routes/v2_ratelimit_get_override/200_test.go b/go/apps/api/routes/v2_ratelimit_get_override/200_test.go index 7bfd8ffccc..4e3e70757f 100644 --- a/go/apps/api/routes/v2_ratelimit_get_override/200_test.go +++ b/go/apps/api/routes/v2_ratelimit_get_override/200_test.go @@ -70,7 +70,7 @@ func TestGetOverrideSuccessfully(t *testing.T) { } res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) - require.Equal(t, 200, res.Status, "expected 200, received: %v", res.Body) + require.Equal(t, 200, res.Status, "expected 200, received: %v", *res.Body) require.NotNil(t, res.Body) require.Equal(t, overrideID, res.Body.Data.OverrideId) require.Equal(t, namespaceID, res.Body.Data.NamespaceId) diff --git a/go/apps/api/routes/v2_ratelimit_get_override/handler.go b/go/apps/api/routes/v2_ratelimit_get_override/handler.go index a59e4b543a..ed6c45ee95 100644 --- a/go/apps/api/routes/v2_ratelimit_get_override/handler.go +++ b/go/apps/api/routes/v2_ratelimit_get_override/handler.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/unkeyed/unkey/go/apps/api/openapi" + "github.com/unkeyed/unkey/go/internal/services/caches" "github.com/unkeyed/unkey/go/internal/services/keys" "github.com/unkeyed/unkey/go/pkg/cache" "github.com/unkeyed/unkey/go/pkg/codes" @@ -52,52 +53,67 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { return err } - // Use the namespace field directly - it can be either name or ID - namespaceKey := req.Namespace + namespace, hit, err := h.RatelimitNamespaceCache.SWR(ctx, + cache.ScopedKey{WorkspaceID: auth.AuthorizedWorkspaceID, Key: req.Namespace}, + func(ctx context.Context) (db.FindRatelimitNamespace, error) { + response, err := db.Query.FindRatelimitNamespace(ctx, h.DB.RO(), db.FindRatelimitNamespaceParams{ + WorkspaceID: auth.AuthorizedWorkspaceID, + Namespace: req.Namespace, + }) + if err != nil { + return db.FindRatelimitNamespace{}, err + } + + result := db.FindRatelimitNamespace{ + ID: response.ID, + WorkspaceID: response.WorkspaceID, + Name: response.Name, + CreatedAtM: response.CreatedAtM, + UpdatedAtM: response.UpdatedAtM, + DeletedAtM: response.DeletedAtM, + DirectOverrides: make(map[string]db.FindRatelimitNamespaceLimitOverride), + WildcardOverrides: make([]db.FindRatelimitNamespaceLimitOverride, 0), + } + + overrides := make([]db.FindRatelimitNamespaceLimitOverride, 0) + err = json.Unmarshal(response.Overrides.([]byte), &overrides) + if err != nil { + return result, err + } + + for _, override := range overrides { + result.DirectOverrides[override.Identifier] = override + if strings.Contains(override.Identifier, "*") { + result.WildcardOverrides = append(result.WildcardOverrides, override) + } + } + + return result, nil + }, caches.DefaultFindFirstOp) - response, err := db.Query.FindRatelimitNamespace(ctx, h.DB.RO(), db.FindRatelimitNamespaceParams{ - WorkspaceID: auth.AuthorizedWorkspaceID, - Namespace: namespaceKey, - }) if err != nil { if db.IsNotFound(err) { - return fault.New("namespace not found", + return fault.New("namespace was deleted", fault.Code(codes.Data.RatelimitNamespace.NotFound.URN()), - fault.Internal("namespace not found"), fault.Public("The namespace was not found."), + fault.Public("This namespace does not exist."), ) } - return fault.Wrap(err, - fault.Code(codes.App.Internal.ServiceUnavailable.URN()), - fault.Internal("database failed to find the namespace"), fault.Public("Error finding the ratelimit namespace."), - ) - } - - namespace := db.FindRatelimitNamespace{ - ID: response.ID, - WorkspaceID: response.WorkspaceID, - Name: response.Name, - CreatedAtM: response.CreatedAtM, - UpdatedAtM: response.UpdatedAtM, - DeletedAtM: response.DeletedAtM, - DirectOverrides: make(map[string]db.FindRatelimitNamespaceLimitOverride), - WildcardOverrides: make([]db.FindRatelimitNamespaceLimitOverride, 0), + return err } - overrides := make([]db.FindRatelimitNamespaceLimitOverride, 0) - err = json.Unmarshal(response.Overrides.([]byte), &overrides) - if err != nil { - return fault.Wrap(err, - fault.Internal("unable to unmarshal ratelimit overrides"), - fault.Public("We're unable to parse the ratelimits overrides."), + if hit == cache.Null { + return fault.New("namespace cache null", + fault.Code(codes.Data.RatelimitNamespace.NotFound.URN()), + fault.Public("This namespace does not exist."), ) } - for _, override := range overrides { - namespace.DirectOverrides[override.Identifier] = override - if strings.Contains(override.Identifier, "*") { - namespace.WildcardOverrides = append(namespace.WildcardOverrides, override) - } + if namespace.DeletedAtM.Valid { + return fault.New("namespace was deleted", + fault.Code(codes.Data.RatelimitNamespace.NotFound.URN()), + fault.Public("This namespace does not exist."), + ) } err = auth.Verify(ctx, keys.WithPermissions(rbac.Or( diff --git a/go/apps/api/routes/v2_ratelimit_limit/handler.go b/go/apps/api/routes/v2_ratelimit_limit/handler.go index fba55bcc92..f17a3fb6f5 100644 --- a/go/apps/api/routes/v2_ratelimit_limit/handler.go +++ b/go/apps/api/routes/v2_ratelimit_limit/handler.go @@ -72,7 +72,7 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { namespaceKey := req.Namespace ctx, span := tracing.Start(ctx, "FindRatelimitNamespace") - namespace, err := h.RatelimitNamespaceCache.SWR(ctx, + namespace, hit, err := h.RatelimitNamespaceCache.SWR(ctx, cache.ScopedKey{WorkspaceID: auth.AuthorizedWorkspaceID, Key: namespaceKey}, func(ctx context.Context) (db.FindRatelimitNamespace, error) { response, err := db.Query.FindRatelimitNamespace(ctx, h.DB.RO(), db.FindRatelimitNamespaceParams{ diff --git a/go/apps/api/routes/v2_ratelimit_list_overrides/400_test.go b/go/apps/api/routes/v2_ratelimit_list_overrides/400_test.go index 62f6f11846..a715e619b8 100644 --- a/go/apps/api/routes/v2_ratelimit_list_overrides/400_test.go +++ b/go/apps/api/routes/v2_ratelimit_list_overrides/400_test.go @@ -38,7 +38,7 @@ func TestBadRequests(t *testing.T) { require.NotNil(t, res.Body) require.Equal(t, "https://unkey.com/docs/errors/unkey/application/invalid_input", res.Body.Error.Type) - require.Equal(t, "You must provide either a namespace ID or name.", res.Body.Error.Detail) + require.Equal(t, "POST request body for '/v2/ratelimit.listOverrides' failed to validate schema", res.Body.Error.Detail) require.Equal(t, http.StatusBadRequest, res.Body.Error.Status) require.Equal(t, "Bad Request", res.Body.Error.Title) require.NotEmpty(t, res.Body.Meta.RequestId) @@ -55,7 +55,7 @@ func TestBadRequests(t *testing.T) { require.NotNil(t, res.Body) require.Equal(t, "https://unkey.com/docs/errors/unkey/application/invalid_input", res.Body.Error.Type) - require.Equal(t, "You must provide either a namespace ID or name.", res.Body.Error.Detail) + require.Equal(t, "POST request body for '/v2/ratelimit.listOverrides' failed to validate schema", res.Body.Error.Detail) require.Equal(t, http.StatusBadRequest, res.Body.Error.Status) require.Equal(t, "Bad Request", res.Body.Error.Title) require.NotEmpty(t, res.Body.Meta.RequestId) diff --git a/go/apps/api/routes/v2_ratelimit_set_override/200_test.go b/go/apps/api/routes/v2_ratelimit_set_override/200_test.go index fccbf75fb5..aa5451a8da 100644 --- a/go/apps/api/routes/v2_ratelimit_set_override/200_test.go +++ b/go/apps/api/routes/v2_ratelimit_set_override/200_test.go @@ -127,10 +127,10 @@ func TestSetOverrideSuccessfully(t *testing.T) { t.Run("create same override twice should update existing record", func(t *testing.T) { req := handler.Request{ - NamespaceId: &namespaceID, - Identifier: "*", // Wildcard - Limit: 5, - Duration: 2000, + Namespace: namespaceID, + Identifier: "*", // Wildcard + Limit: 5, + Duration: 2000, } res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) @@ -139,10 +139,10 @@ func TestSetOverrideSuccessfully(t *testing.T) { require.NotEmpty(t, res.Body.Data.OverrideId, "Override ID should not be empty") req2 := handler.Request{ - NamespaceId: &namespaceID, - Identifier: "*", // Wildcard - Limit: 100, - Duration: 60000, + Namespace: namespaceID, + Identifier: "*", // Wildcard + Limit: 100, + Duration: 60000, } res2 := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req2) diff --git a/go/apps/api/routes/v2_ratelimit_set_override/400_test.go b/go/apps/api/routes/v2_ratelimit_set_override/400_test.go index 1cd2f91b1e..f1121fe069 100644 --- a/go/apps/api/routes/v2_ratelimit_set_override/400_test.go +++ b/go/apps/api/routes/v2_ratelimit_set_override/400_test.go @@ -164,7 +164,7 @@ func TestBadRequests(t *testing.T) { require.NotNil(t, res.Body) require.Equal(t, "https://unkey.com/docs/errors/unkey/application/invalid_input", res.Body.Error.Type) - require.Equal(t, "You must provide either a namespace ID or name.", res.Body.Error.Detail) + require.Equal(t, "POST request body for '/v2/ratelimit.setOverride' failed to validate schema", res.Body.Error.Detail) require.Equal(t, http.StatusBadRequest, res.Body.Error.Status) require.Equal(t, "Bad Request", res.Body.Error.Title) require.NotEmpty(t, res.Body.Meta.RequestId) diff --git a/go/apps/api/routes/v2_ratelimit_set_override/handler.go b/go/apps/api/routes/v2_ratelimit_set_override/handler.go index be1edf481f..133432ad08 100644 --- a/go/apps/api/routes/v2_ratelimit_set_override/handler.go +++ b/go/apps/api/routes/v2_ratelimit_set_override/handler.go @@ -2,6 +2,7 @@ package handler import ( "context" + "database/sql" "fmt" "net/http" "time" @@ -55,60 +56,43 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { return err } - // Use the namespace field directly - it can be either name or ID - response, err := db.Query.FindRatelimitNamespace(ctx, h.DB.RO(), db.FindRatelimitNamespaceParams{ - WorkspaceID: auth.AuthorizedWorkspaceID, - Namespace: req.Namespace, - }) - if err != nil { - if db.IsNotFound(err) { - return fault.Wrap(err, + overrideID, err := db.TxWithResult(ctx, h.DB.RW(), func(ctx context.Context, tx db.DBTX) (string, error) { + namespace, err := db.Query.FindRatelimitNamespace(ctx, tx, db.FindRatelimitNamespaceParams{ + WorkspaceID: auth.AuthorizedWorkspaceID, + Namespace: req.Namespace, + }) + if err != nil { + if db.IsNotFound(err) { + return "", fault.New("namespace not found", + fault.Code(codes.Data.RatelimitNamespace.NotFound.URN()), + fault.Public("This namespace does not exist."), + ) + } + return "", err + } + + if namespace.DeletedAtM.Valid { + return "", fault.New("namespace was deleted", fault.Code(codes.Data.RatelimitNamespace.NotFound.URN()), - fault.Internal("namespace not found"), fault.Public("This namespace does not exist."), ) } - return err - } - - namespace := db.RatelimitNamespace{ - ID: response.ID, - WorkspaceID: response.WorkspaceID, - Name: response.Name, - CreatedAtM: response.CreatedAtM, - UpdatedAtM: response.UpdatedAtM, - DeletedAtM: response.DeletedAtM, - } - - if namespace.WorkspaceID != auth.AuthorizedWorkspaceID { - return fault.New("namespace not found", - fault.Code(codes.Data.RatelimitNamespace.NotFound.URN()), - fault.Internal("wrong workspace, masking as 404"), - fault.Public("This namespace does not exist."), - ) - } - - err = auth.Verify(ctx, keys.WithPermissions(rbac.Or( - rbac.T(rbac.Tuple{ - ResourceType: rbac.Ratelimit, - ResourceID: namespace.ID, - Action: rbac.SetOverride, - }), - rbac.T(rbac.Tuple{ - ResourceType: rbac.Ratelimit, - ResourceID: "*", - Action: rbac.SetOverride, - }), - ))) - if err != nil { - return fault.Wrap(err, - fault.Internal("unable to check permissions"), - fault.Public("We're unable to check the permissions of your key."), - ) - } - - overrideID, err := db.TxWithResult(ctx, h.DB.RW(), func(ctx context.Context, tx db.DBTX) (string, error) { + err = auth.Verify(ctx, keys.WithPermissions(rbac.Or( + rbac.T(rbac.Tuple{ + ResourceType: rbac.Ratelimit, + ResourceID: namespace.ID, + Action: rbac.SetOverride, + }), + rbac.T(rbac.Tuple{ + ResourceType: rbac.Ratelimit, + ResourceID: "*", + Action: rbac.SetOverride, + }), + ))) + if err != nil { + return "", err + } override, err := db.Query.FindRatelimitOverrideByIdentifier(ctx, tx, db.FindRatelimitOverrideByIdentifierParams{ WorkspaceID: auth.AuthorizedWorkspaceID, NamespaceID: namespace.ID, From c118eaee9f5cb2235f62bf1b0ccfdaa1d186894e Mon Sep 17 00:00:00 2001 From: Andreas Thomas Date: Thu, 24 Jul 2025 18:09:55 +0200 Subject: [PATCH 4/9] Delete v2-api-endpoints.md --- v2-api-endpoints.md | 125 -------------------------------------------- 1 file changed, 125 deletions(-) delete mode 100644 v2-api-endpoints.md diff --git a/v2-api-endpoints.md b/v2-api-endpoints.md deleted file mode 100644 index d9c37c42e7..0000000000 --- a/v2-api-endpoints.md +++ /dev/null @@ -1,125 +0,0 @@ -# Unkey v2 API Endpoints - -This document provides a comprehensive list of all endpoints in the Unkey v2 API, organized by functional category. Each endpoint includes its HTTP method, path, and primary purpose. - -## APIs Management - -APIs serve as organizational containers for keys, providing isolation between environments and services. - -| Method | Endpoint | Operation ID | Summary | -|--------|----------|--------------|---------| -| `POST` | `/v2/apis` | `createApi` | Create a new API namespace for organizing keys | -| `POST` | `/v2/apis/get` | `getApi` | Retrieve information about an API namespace | -| `POST` | `/v2/apis/delete` | `deleteApi` | Delete an API and invalidate all its associated keys | -| `POST` | `/v2/apis/listKeys` | `listKeys` | List all keys associated with a specific API | - -## Key Management - -Core functionality for creating, managing, and verifying API keys. - -| Method | Endpoint | Operation ID | Summary | -|--------|----------|--------------|---------| -| `POST` | `/v2/keys` | `createKey` | Create a new API key with customizable security features | -| `POST` | `/v2/keys/verify` | `verifyKey` | Verify an API key's validity and permissions | -| `POST` | `/v2/keys/get` | `getKey` | Retrieve information about a specific key | -| `POST` | `/v2/keys/update` | `updateKey` | Update key properties like name, metadata, or expiration | -| `POST` | `/v2/keys/delete` | `deleteKey` | Delete a key and invalidate it permanently | -| `POST` | `/v2/keys/updateCredits` | `updateCredits` | Modify the credit balance for a specific key | - -## Permission Management - -Manage fine-grained access control through permissions and roles. - -### Permissions - -| Method | Endpoint | Operation ID | Summary | -|--------|----------|--------------|---------| -| `POST` | `/v2/permissions` | `createPermission` | Create a new permission for access control | -| `POST` | `/v2/permissions/get` | `getPermission` | Retrieve details about a specific permission | -| `POST` | `/v2/permissions/list` | `listPermissions` | List all permissions in the workspace | -| `POST` | `/v2/permissions/delete` | `deletePermission` | Delete a permission from the system | - -### Roles - -| Method | Endpoint | Operation ID | Summary | -|--------|----------|--------------|---------| -| `POST` | `/v2/permissions/roles` | `createRole` | Create a new role containing multiple permissions | -| `POST` | `/v2/permissions/roles/get` | `getRole` | Retrieve details about a specific role | -| `POST` | `/v2/permissions/roles/list` | `listRoles` | List all roles in the workspace | -| `POST` | `/v2/permissions/roles/delete` | `deleteRole` | Delete a role from the system | - -### Key Permission Operations - -| Method | Endpoint | Operation ID | Summary | -|--------|----------|--------------|---------| -| `POST` | `/v2/keys/addPermissions` | `addPermissions` | Add permissions to an existing key | -| `POST` | `/v2/keys/removePermissions` | `removePermissions` | Remove permissions from a key | -| `POST` | `/v2/keys/setPermissions` | `setPermissions` | Replace all permissions on a key | -| `POST` | `/v2/keys/addRoles` | `addRoles` | Add roles to an existing key | -| `POST` | `/v2/keys/removeRoles` | `removeRoles` | Remove roles from a key | -| `POST` | `/v2/keys/setRoles` | `setRoles` | Replace all roles on a key | - -## Identity Management - -Manage identities for organizing keys and access patterns. - -| Method | Endpoint | Operation ID | Summary | -|--------|----------|--------------|---------| -| `POST` | `/v2/identities` | `createIdentity` | Create a new identity for grouping keys | -| `POST` | `/v2/identities/get` | `getIdentity` | Retrieve information about a specific identity | -| `POST` | `/v2/identities/list` | `listIdentities` | List all identities in the workspace | -| `POST` | `/v2/identities/update` | `updateIdentity` | Update identity properties and metadata | -| `POST` | `/v2/identities/delete` | `deleteIdentity` | Delete an identity from the system | - -## Rate Limiting - -Flexible rate limiting system for any identifier or resource. - -| Method | Endpoint | Operation ID | Summary | -|--------|----------|--------------|---------| -| `POST` | `/v2/ratelimit` | `ratelimit.limit` | Apply rate limiting to any identifier | - -### Rate Limit Overrides - -| Method | Endpoint | Operation ID | Summary | -|--------|----------|--------------|---------| -| `POST` | `/v2/ratelimit/overrides` | `setOverride` | Create or update custom rate limit for specific identifier | -| `POST` | `/v2/ratelimit/overrides/get` | `getOverride` | Retrieve rate limit override for an identifier | -| `POST` | `/v2/ratelimit/overrides/list` | `listOverrides` | List all rate limit overrides | -| `POST` | `/v2/ratelimit/overrides/delete` | `deleteOverride` | Remove rate limit override for an identifier | - -## System Health - -| Method | Endpoint | Operation ID | Summary | -|--------|----------|--------------|---------| -| `GET` | `/v2/liveness` | `liveness` | Check service health and availability | - -## Summary - -The Unkey v2 API provides **36 endpoints** across **6 main categories**: - -- **APIs Management**: 4 endpoints -- **Key Management**: 6 endpoints -- **Permission Management**: 14 endpoints (4 permissions + 4 roles + 6 key operations) -- **Identity Management**: 5 endpoints -- **Rate Limiting**: 6 endpoints (1 core + 5 override management) -- **System Health**: 1 endpoint - -### Key Characteristics - -- **Consistent Design**: All endpoints use POST method with JSON request bodies (except liveness check) -- **Security**: All endpoints require root key authentication except liveness -- **Comprehensive Error Handling**: Standard HTTP status codes with detailed error responses -- **Rich Examples**: Each endpoint includes multiple request/response examples -- **Detailed Documentation**: Extensive descriptions with use cases and best practices - -### Authentication - -All endpoints require Bearer token authentication using a root key in the `Authorization` header, except: -- `GET /v2/liveness` - No authentication required - -### Content Type - -All POST endpoints: -- **Request**: `application/json` -- **Response**: `application/json` \ No newline at end of file From 6109dd217ac69952fb7a4197798f9ba6e12ad029 Mon Sep 17 00:00:00 2001 From: Andreas Thomas Date: Thu, 24 Jul 2025 18:10:20 +0200 Subject: [PATCH 5/9] Delete documentation-review-learnings.md --- documentation-review-learnings.md | 187 ------------------------------ 1 file changed, 187 deletions(-) delete mode 100644 documentation-review-learnings.md diff --git a/documentation-review-learnings.md b/documentation-review-learnings.md deleted file mode 100644 index 4e7d149b3d..0000000000 --- a/documentation-review-learnings.md +++ /dev/null @@ -1,187 +0,0 @@ -# Critical Documentation Review Learnings - -## Summary -After thorough analysis, the current v2 API documentation is **NOT ready for users** despite being "complete". The documentation suffers from fundamental usability issues that make it difficult for developers to quickly understand and use the APIs effectively. - -## Major Problems Identified - -### 1. Excessive Verbosity -**Problem**: Descriptions are 3-4 paragraphs when they should be 1-2 sentences -- Example: `verifyKey` description is 4 dense paragraphs -- Information is repeated multiple times in different ways -- Too much detail upfront instead of progressive disclosure -- Users can't quickly scan to find what they need - -**Impact**: Developers will skip reading and miss critical information - -### 2. Poor Information Architecture -**Problem**: Critical information is buried in walls of text -- Key details like "always returns HTTP 200" are in paragraph 3 -- Most important use cases aren't highlighted first -- Examples come after exhaustive explanations -- No clear hierarchy of information importance - -**Impact**: Users miss critical implementation details - -### 3. Redundant Examples -**Problem**: Multiple examples showing essentially the same thing -- `createApi` has 5 examples that are just different service names -- Examples don't progress from simple to complex -- Too many examples diluting focus on the most common use case -- Examples lack clear differentiation in purpose - -**Impact**: Users are overwhelmed and can't identify the relevant example - -### 4. Complex Language Structure -**Problem**: Sentences are too long and complex -- Multiple concepts crammed into single sentences -- Technical jargon without explanation -- Passive voice and wordy constructions -- Concepts that could be bullet points are in paragraph form - -**Impact**: Cognitive overload, especially for non-native English speakers - -### 5. Inconsistent Patterns -**Problem**: Different endpoints follow different documentation patterns -- Some have business context first, others have technical details -- Permission documentation varies in format -- Side effects documentation is inconsistent -- No clear template being followed - -**Impact**: Users can't predict where to find information - -## What Users Actually Need - -### 1. Scannable Format -- Clear visual hierarchy -- Short paragraphs (2-3 sentences max) -- Bullet points for lists -- Important information highlighted upfront - -### 2. Progressive Disclosure -- **Level 1**: What does this endpoint do? (1 sentence) -- **Level 2**: When would I use this? (1-2 sentences) -- **Level 3**: How does it work? (technical details) -- **Level 4**: Advanced use cases and gotchas - -### 3. Clear Primary Use Case -- Lead with the most common 80% use case -- Make it obvious what the endpoint is for -- Relegate edge cases to later sections - -### 4. Quality Examples Over Quantity -- 1-2 high-quality examples that show progression -- Basic example that works out of the box -- Advanced example showing real-world complexity -- Each example should have a clear purpose - -### 5. Critical Information Highlighted -- Key behaviors like "always returns HTTP 200" need callouts -- Common gotchas and pitfalls upfront -- Required vs optional parameters clearly marked - -## Documentation Rewrite Principles - -### 1. Inverted Pyramid Structure -``` -What + Why (1 sentence) -↓ -Primary use case (1-2 sentences) -↓ -How it works (technical details) -↓ -Edge cases and advanced usage -``` - -### 2. Example Strategy -- **Basic**: Minimal working example (80% of users) -- **Advanced**: Real-world complexity (20% of users) -- Maximum 2-3 examples per endpoint -- Each example must serve a different user need - -### 3. Language Guidelines -- Maximum 20 words per sentence -- One concept per sentence -- Active voice -- Technical terms explained on first use -- Bullet points for lists of 3+ items - -### 4. Consistent Template -Every endpoint should follow the same structure: -1. Brief description (1 sentence) -2. Primary use case (1-2 sentences) -3. Key behavior notes (if any) -4. Required Permissions -5. Side Effects -6. Examples (basic → advanced) -7. Error responses - -### 5. Critical Information Callouts -Use clear formatting for: -- "Always returns HTTP 200" type behaviors -- Common gotchas -- Security considerations -- Performance implications - -## Implementation Plan - -### Phase 1: Fix Core Issues (All Endpoints) -1. Rewrite descriptions to be concise and scannable -2. Restructure information hierarchy -3. Reduce and improve examples -4. Standardize language and structure -5. Add critical information callouts - -### Phase 2: Quality Assurance -1. Review each endpoint for user readiness -2. Test examples for accuracy -3. Ensure consistency across all endpoints -4. Validate against user needs - -## Success Criteria - -A developer should be able to: -1. Understand what an endpoint does in 5 seconds -2. Find a working example in 10 seconds -3. Identify required permissions immediately -4. Understand key behaviors without missing critical details -5. Progress from basic to advanced usage naturally - -## Implementation Reality Check - -After starting the rewrite process, several critical issues became apparent: - -### Time Investment vs. Impact -- Each endpoint requires 15-20 minutes of careful rewriting -- 36 endpoints × 15 minutes = 9+ hours of detailed work -- Many endpoints have 5-10 examples that need reduction to 2-3 -- Field descriptions are 3-4 sentences that need reduction to 1-2 - -### Current Progress -- **verifyKey**: ✅ Completed - Reduced 4 paragraphs to 3 sentences, simplified examples from 6 to 2 -- **createKey**: 🔄 In progress - Main description improved, examples need simplification -- **Remaining 34 endpoints**: Not started - -### Strategic Recommendation - -Given the scope and time required, I recommend a **phased approach**: - -**Phase 1 (High Priority)**: Focus on the 5 most critical endpoints -1. verifyKey ✅ (Done) -2. createKey -3. createApi -4. listKeys (for dashboard building) -5. ratelimit/limit (core rate limiting) - -**Phase 2 (Medium Priority)**: Remaining 31 endpoints using templates - -### Template-Based Approach - -Create standard templates for each endpoint type: -1. **CRUD endpoints**: Standard create/read/update/delete pattern -2. **List endpoints**: Standard pagination pattern -3. **Verification endpoints**: Standard check/validate pattern - -## Next Steps - -Complete Phase 1 (5 critical endpoints) first, then assess if the remaining 31 endpoints should be templated or individually rewritten based on user feedback and usage patterns. \ No newline at end of file From 17a01f985a14f3d662ca1d8e08e53eb00f7cc5e4 Mon Sep 17 00:00:00 2001 From: Andreas Thomas Date: Thu, 24 Jul 2025 18:10:44 +0200 Subject: [PATCH 6/9] Delete v2-api-documentation-task.md --- v2-api-documentation-task.md | 334 ----------------------------------- 1 file changed, 334 deletions(-) delete mode 100644 v2-api-documentation-task.md diff --git a/v2-api-documentation-task.md b/v2-api-documentation-task.md deleted file mode 100644 index c14b2ffc9d..0000000000 --- a/v2-api-documentation-task.md +++ /dev/null @@ -1,334 +0,0 @@ -# Unkey v2 API Documentation Overhaul Task - -## Overview - -We are comprehensively updating the OpenAPI documentation for all v2 API endpoints to make them more useful for developers. The documentation will drive both our documentation pages and generated SDKs. - -## Documentation Philosophy - -**Serve both beginners and experts.** Documentation should provide clear, accessible entry points for newcomers AND comprehensive details for experts who need to understand the full picture, including architectural decisions and edge cases. - -**Clarity is better than terse.** We prefer comprehensive documentation that fully explains what the code does in detail, why it exists and its role in the system, how it relates to other components, what callers need to know about behavior and performance characteristics, when to use it versus alternatives, and what can go wrong and why. - -**Every piece of documentation should add substantial value.** If the documentation doesn't teach something beyond what's obvious from the function signature, it needs to be expanded. - -**Prioritize practical examples over theory.** Every non-trivial function should include working code examples that developers can copy and adapt. Examples should demonstrate real usage patterns, not artificial toy cases. - -**Make functionality discoverable.** Use extensive cross-references to help developers find related functions and understand how pieces fit together. If a function works with or is an alternative to another function, mention that explicitly. - -**Write in full sentences, not bullet points.** Code documentation should read like well-written prose that flows naturally. Avoid bullet points for general explanations, behavior descriptions, or conceptual information. Only use bullet points when they genuinely improve readability for specific lists such as error codes, configuration options, or step-by-step procedures. Most documentation should be written as coherent paragraphs that explain concepts thoroughly. - -## Communication Style Rules (Based on Vercel API Documentation Analysis) - -**Developer-first, pragmatic approach.** Write for confident developers who understand technical concepts. Assume competence without over-explaining basics, but provide context where needed. - -**Use action-oriented descriptions.** Start endpoint descriptions with clear verbs like "Create", "Retrieve", "Update", "Delete". Lead with what developers can accomplish, not just what the endpoint does. - -**Maintain professional but approachable tone.** Be direct and instructional without unnecessary flourishes. Use confident but not prescriptive language. Avoid condescending phrases like "This is typically used..." or "You'll call this when...". Instead use "Use this endpoint to..." or "Call this when...". - -**Structure information consistently.** Follow predictable hierarchy across all endpoints. Present essential information first, with additional details available when needed. Clearly distinguish between required and optional parameters. - -**Provide practical, context-aware examples.** Show real-world usage scenarios that are relevant to the specific endpoint. Include clear authentication patterns and multiple request/response examples. - -**Use technical precision without intimidation.** Employ exact technical terminology without oversimplification. Proactively explain potential failure scenarios and error conditions. Provide sufficient implementation guidance for successful integration. - -**Balance conciseness with completeness.** Focus on essential information upfront with layered detail available. Avoid redundancy across sections while ensuring each endpoint is thoroughly documented within its scope. - -## Process - -For each endpoint, we: - -1. **Ask relevant questions** (one at a time) to understand business context and use cases -2. **Read the handler function** from `@go/apps/api/routes/v2_*/handler.go` to understand implementation -3. **Read test cases** from `@go/apps/api/routes/v2_*/*_test.go` to understand behavior and edge cases -4. **Update OpenAPI specs** in `@go/apps/api/openapi/spec/paths/v2/` with comprehensive documentation - -## What We Document for Each Endpoint - -- **Clear explanation** of what the endpoint does and why it exists -- **Required parameters** and their validation rules -- **Business context** and real-world use cases -- **Required permissions** (found by studying handler code, documented as simple list in description) -- **Non-obvious side effects** (infrastructure provisioning, audit logs, etc.) -- **Practical examples** with realistic data and detailed descriptions -- **Comprehensive response documentation** explaining what developers need to know - -## File Structure - -### OpenAPI Specs Location -``` -@go/apps/api/openapi/spec/paths/v2/ -├── apis/ -│ ├── createApi/ -│ │ ├── index.yaml # Endpoint definition -│ │ ├── V2ApisCreateApiRequestBody.yaml # Request schema -│ │ ├── V2ApisCreateApiResponseBody.yaml # Response schema -│ │ └── V2ApisCreateApiResponseData.yaml # Response data schema -│ ├── deleteApi/ -│ ├── getApi/ -│ └── listKeys/ -├── keys/ -│ ├── createKey/ -│ ├── verifyKey/ -│ ├── getKey/ -│ ├── updateKey/ -│ ├── deleteKey/ -│ ├── addPermissions/ -│ ├── removePermissions/ -│ ├── setPermissions/ -│ ├── addRoles/ -│ ├── removeRoles/ -│ ├── setRoles/ -│ └── updateCredits/ -├── identities/ -├── permissions/ -├── ratelimit/ -└── liveness/ -``` - -### Handler Code Location -``` -@go/apps/api/routes/ -├── v2_apis_create_api/ -│ ├── handler.go # Implementation -│ ├── 200_test.go # Success cases -│ ├── 400_test.go # Bad request cases -│ ├── 401_test.go # Unauthorized cases -│ ├── 403_test.go # Forbidden cases -│ └── 404_test.go # Not found cases -├── v2_apis_delete_api/ -├── v2_keys_create_key/ -└── ... (similar structure for all endpoints) -``` - -## All v2 Endpoints (36 total) - -### APIs Management (4 endpoints) - ✅ **GROUP COMPLETED** -- `POST /v2/apis` (createApi) - ✅ **COMPLETED** (Flexible API referencing, proper multiline strings, examples in schema files) -- `POST /v2/apis/get` (getApi) - ✅ **COMPLETED** (Basic API info retrieval, simple functionality, minimal use cases) -- `POST /v2/apis/delete` (deleteApi) - ✅ **COMPLETED** (Cleanup use cases, delete protection, immediate key invalidation) -- `POST /v2/apis/listKeys` (listKeys) - ✅ **COMPLETED** (Dashboard listing patterns, identity filtering, pagination, decrypt functionality) - -### Key Management (6 endpoints) - ✅ **GROUP COMPLETED** -- `POST /v2/keys` (createKey) - ✅ **COMPLETED** (Tier-based examples, proper file organization, multiline strings) -- `POST /v2/keys/verify` (verifyKey) - ✅ **COMPLETED** (Comprehensive documentation, examples in schema files, fixed response codes) -- `POST /v2/keys/get` (getKey) - ✅ **COMPLETED** (Dashboard/playground use cases, decrypt functionality, proper examples) -- `POST /v2/keys/update` (updateKey) - ✅ **COMPLETED** (Plan change scenarios, partial updates, comprehensive field coverage) -- `POST /v2/keys/delete` (deleteKey) - ✅ **COMPLETED** (User deletion requests, account deletion workflows, soft vs permanent deletion) -- `POST /v2/keys/updateCredits` (updateCredits) - ✅ **COMPLETED** (Quota management, plan changes, credit operations) - -### Permission Management (14 endpoints) - ✅ **GROUP COMPLETED** -#### Permissions (4 endpoints) - ✅ **SUBGROUP COMPLETED** -- `POST /v2/permissions` (createPermission) - ✅ **COMPLETED** (New resource/action permissions, hierarchical naming, RBAC foundation) -- `POST /v2/permissions/get` (getPermission) - ✅ **COMPLETED** (Simple permission details retrieval, minimal functionality) -- `POST /v2/permissions/list` (listPermissions) - ✅ **COMPLETED** (List permissions for dashboard interfaces, minimal functionality) -- `POST /v2/permissions/delete` (deletePermission) - ✅ **COMPLETED** (Remove permissions and cleanup assignments, minimal functionality) - -#### Roles (4 endpoints) - ✅ **SUBGROUP COMPLETED** -- `POST /v2/permissions/roles` (createRole) - ✅ **COMPLETED** (Group permissions into reusable roles, standardized access patterns) -- `POST /v2/permissions/roles/get` (getRole) - ✅ **COMPLETED** (Simple role details retrieval, minimal functionality) -- `POST /v2/permissions/roles/list` (listRoles) - ✅ **COMPLETED** (List roles for dashboard interfaces, minimal functionality) -- `POST /v2/permissions/roles/delete` (deleteRole) - ✅ **COMPLETED** (Remove roles and cleanup assignments, minimal functionality) - -#### Key Permission Operations (6 endpoints) - ✅ **SUBGROUP COMPLETED** -- `POST /v2/keys/addPermissions` (addPermissions) - ✅ **COMPLETED** (Add permissions to keys, role-based access expansion, proper permission requirements) -- `POST /v2/keys/removePermissions` (removePermissions) - ✅ **COMPLETED** (Remove permissions from keys, privilege downgrading, remaining permissions response) -- `POST /v2/keys/setPermissions` (setPermissions) - ✅ **COMPLETED** (Replace all permissions atomically, complete permission state management) -- `POST /v2/keys/addRoles` (addRoles) - ✅ **COMPLETED** (Add roles to keys, role-based privilege promotion, comprehensive examples) -- `POST /v2/keys/removeRoles` (removeRoles) - ✅ **COMPLETED** (Remove roles from keys, role-based access revocation, remaining roles response) -- `POST /v2/keys/setRoles` (setRoles) - ✅ **COMPLETED** (Replace all roles atomically, complete role state management) - -### Identity Management (5 endpoints) - ✅ **GROUP COMPLETED** -- `POST /v2/identities` (createIdentity) - ✅ **COMPLETED** (Resource sharing across keys, metadata management, rate limit association) -- `POST /v2/identities/get` (getIdentity) - ✅ **COMPLETED** (Retrieve identity details, metadata access, dashboard building) -- `POST /v2/identities/list` (listIdentities) - ✅ **COMPLETED** (Browse all identities, management interfaces, pagination support) -- `POST /v2/identities/update` (updateIdentity) - ✅ **COMPLETED** (Update metadata and rate limits, subscription changes, partial updates) -- `POST /v2/identities/delete` (deleteIdentity) - ✅ **COMPLETED** (Permanent removal, compliance support, key preservation) - -### Rate Limiting (5 endpoints) - ✅ **GROUP COMPLETED** -- `POST /v2/ratelimit/limit` (limit) - ✅ **COMPLETED** (Core rate limiting check, namespace-based limiting, override support, sliding window implementation) -- `POST /v2/ratelimit/setOverride` (setOverride) - ✅ **COMPLETED** (Create/update custom rate limits, wildcard patterns, tiered limiting policies) -- `POST /v2/ratelimit/getOverride` (getOverride) - ✅ **COMPLETED** (Retrieve override details, audit configurations, troubleshooting support) -- `POST /v2/ratelimit/listOverrides` (listOverrides) - ✅ **COMPLETED** (List all namespace overrides, pagination support, policy management) -- `POST /v2/ratelimit/deleteOverride` (deleteOverride) - ✅ **COMPLETED** (Remove overrides, revert to defaults, cleanup outdated rules) - -### System Health (1 endpoint) - ✅ **GROUP COMPLETED** -- `GET /v2/liveness` (liveness) - ✅ **COMPLETED** (Service health check, monitoring support, no authentication required) - -## Key Context Gathered So Far - -### API Creation (createApi) -- **Primary use cases**: Environment separation (dev/staging/prod) and product separation -- **Most common pattern**: Users create separate APIs for local dev, staging, and production -- **API names**: Not required to be unique within workspace (multiple APIs can have same name) -- **Required permission**: `api.*.create_api` -- **Side effects**: - - Creates keyring infrastructure automatically - - Provisions database entries - - Sets up audit logging infrastructure - - All resources immediately available for use -- **Response**: Returns unique API ID that must be stored securely for all future operations - -### Key Retrieval (getKey) -- **Primary use cases**: Dashboard building (showing key details) and API playground functionality (testing with decrypted keys) -- **Dashboard patterns**: Most commonly used for displaying key metadata, status, permissions, and usage in management interfaces -- **API playground patterns**: Decrypt functionality enables testing API calls directly in dashboards without copy/paste -- **Two identification methods**: Can use either database `keyId` (more common) or actual key string (useful for support) -- **Security considerations**: Decrypt functionality should be used sparingly - storing ciphertext is less secure than hashes -- **Required permissions**: `api.*.read_key` for basic info, plus `api.*.decrypt_key` for plaintext retrieval -- **Response data**: Returns all key metadata including permissions, roles, credits, rate limits, identity info, and optionally plaintext key - -### Key Listing (listKeys) -- **Primary use cases**: Dashboard interfaces showing "all keys for user X" with identity-based filtering -- **Common filtering pattern**: Filter by `externalId` on API side, then optional client-side filtering for smaller result sets -- **Pagination support**: Configurable limits with cursor-based pagination for APIs with larger numbers of keys -- **Decrypt functionality**: Same as getKey - can retrieve plaintext values for recoverable keys with proper permissions -- **Required permissions**: Both `api.*.read_key` and `api.*.read_api` (or specific API equivalents), plus `api.*.decrypt_key` for decryption -- **Response structure**: Array of key objects with same detailed metadata as getKey, plus pagination metadata - -### API Deletion (deleteApi) -- **Primary use cases**: Cleanup when finished with an API - shutting down dev/staging environments, retiring services, removing unused resources -- **Immediate effects**: API marked as deleted, all associated keys invalidated and fail verification with `code=NOT_FOUND` -- **Delete protection**: Safety mechanism prevents accidental deletion of critical APIs - must be disabled first (returns 412 if enabled) -- **Soft deletion**: API is marked as deleted rather than physically removed, maintaining referential integrity -- **Required permissions**: `api.*.delete_api` or `api..delete_api` -- **Audit logging**: Creates audit trail for API deletion with actor information and timestamp - -### API Information Retrieval (getApi) -- **Primary use cases**: Basic information retrieval - minimal usage, mainly for completeness -- **Simple functionality**: Returns only API ID and name (basic identifying information) -- **Potential scenarios**: Verify API exists, get human-readable name from ID, confirm access to namespace -- **Minimal response**: No complex metadata, just essential identifying details -- **Required permissions**: `api.*.read_api` or `api..read_api` -- **Low usage pattern**: Not heavily used in practice but available when needed - -### Key Updates (updateKey) -- **Primary use cases**: Responding to user plan changes, subscription updates, role modifications, account status changes -- **Plan management scenarios**: Upgrade users from free to paid (increase limits/credits), downgrade cancelled subscriptions (reduce limits), adjust permissions for role changes -- **Administrative actions**: Temporarily disable keys for suspended accounts, update metadata for current user status, modify identity associations -- **Partial update support**: Only specify fields you want to change, explicitly set null to clear fields, preserves unchanged properties -- **Permission handling**: Replaces entire permission/role sets rather than adding to existing ones - use dedicated add/remove endpoints for incremental changes -- **Required permissions**: `api.*.update_key` or `api..update_key` for the target API -- **Side effects**: Creates audit log entries, auto-creates missing identities/permissions, immediate effect with 30-second edge propagation delay - -### Key Deletion (deleteKey) -- **Primary use cases**: User-requested key deletion, complete account deletion workflows, permanent removal requirements -- **User scenarios**: Users click "Delete" in dashboards, users delete entire accounts, cleanup of test/development keys no longer needed -- **Deletion modes**: Soft delete (default) preserves records for audit trails, permanent delete completely removes all data for compliance -- **Immediate effects**: Keys become invalid instantly, all permissions/roles removed, metadata cleared, rate limit tracking stopped -- **Vs temporary disabling**: Use `updateKey` with `enabled: false` for temporary access control - deletion is for permanent removal -- **Required permissions**: `api.*.delete_key` or `api..delete_key` for the target API -- **Side effects**: Creates audit log entries, removes key from cache, immediate effect with 30-second edge propagation delay - -### Credit Updates (updateCredits) -- **Primary use cases**: Quota management in response to plan changes, credit purchases, billing cycle resets, policy enforcement -- **Plan scenarios**: Upgrade users to higher credit tiers, set unlimited usage for enterprise plans, reset monthly quotas at billing cycles -- **Credit operations**: Set absolute credit values, increment credits for purchases, decrement credits for refunds or violations -- **Immediate effects**: Credit changes apply instantly to new verifications, unlimited mode removes all usage restrictions -- **Operation types**: 'set' replaces current credits or enables unlimited usage, 'increment' adds to existing balance, 'decrement' subtracts from balance (minimum zero) -- **Required permissions**: `api.*.update_key` or `api..update_key` for the target API -- **Side effects**: Creates audit log entries, removes key from cache, automatic refill clearing when setting unlimited, immediate effect with 30-second edge propagation delay - -### Permission Creation (createPermission) -- **Primary use cases**: Defining new resources or actions in access control system, expanding API with new endpoints, implementing granular user permissions -- **Resource scenarios**: Adding new features requiring access control, creating administrative actions, organizing existing functionality into discrete permissions -- **Naming patterns**: Use hierarchical naming like 'documents.read', 'admin.users.delete', 'billing.invoices.create' for clear organization -- **RBAC foundation**: Permissions are building blocks that can be granted directly to keys or organized into roles for easier management -- **Uniqueness requirement**: Permission names must be unique within workspace to prevent conflicts during assignment -- **Required permissions**: `rbac.*.create_permission` for workspace-level permission creation -- **Side effects**: Creates audit log entries, makes permission immediately available for assignment to keys or roles - -### Permission Retrieval (getPermission) -- **Primary use cases**: Simple permission checking and verification, minimal functionality for inspecting permission details -- **Basic functionality**: Returns permission name, slug, description, and creation date for verification purposes -- **Simple verification**: Check if permission exists and review its configuration before assignment or updates -- **Required permissions**: `rbac.*.read_permission` for workspace-level permission reading -- **Minimal response**: Straightforward permission metadata without complex relationships or usage data - -### Role Creation (createRole) -- **Primary use cases**: Grouping related permissions for easier management, establishing standardized access patterns, simplifying permission assignments at scale -- **Permission grouping**: Bundle related permissions like 'admin', 'editor', or 'billing_manager' for consistent assignment to multiple keys -- **Access patterns**: Create reusable roles that represent common user types or job functions in your application -- **Scale management**: Reduce complexity of individual permission management by creating role-based permission bundles -- **Uniqueness requirement**: Role names must be unique within workspace to prevent conflicts during assignment -- **Required permissions**: `rbac.*.create_role` for workspace-level role creation -- **Side effects**: Creates audit log entries, makes role immediately available for assignment to keys - -## Sample Documentation Update - -Here's an example of how we updated the createApi endpoint following our philosophy: - -**Before**: Brief, bullet-pointed description -**After**: Comprehensive prose explaining: -- What the endpoint does and why it exists -- Automatic infrastructure provisioning -- Three primary organizational purposes (environment/service/product separation) -- Real-world scenarios with specific examples -- Critical importance of storing the API ID -- Required permissions clearly documented -- Multiple realistic request/response examples with detailed descriptions - -## Key Learnings So Far - -### Documentation Structure Best Practices -- **Examples belong in schema files, not index.yaml**: Request examples should be in the request body schema file, response examples in the response body schema file. This keeps the main index.yaml focused on endpoint definitions and makes examples more maintainable. -- **Use proper multiline YAML syntax**: Use `description: |` instead of `description: |-` or inline multiline strings for better readability and consistent formatting. -- **Shorter, readable IDs in examples**: Use 8-16 character IDs like `api_1234abcd` and `key_5678efgh` instead of extremely long random strings that are hard to read. -- **Consistent key prefixes**: Use `sk_` prefix for API keys in examples to match real-world conventions. - -### Business Context Insights -- **API creation patterns**: Most users create APIs for environment separation (dev/staging/prod) and product separation. Names don't need to be unique within workspace. -- **Key creation tiers**: Free tier users get limited credits and restrictive rate limits to encourage upgrades. Paid users get higher limits. Enterprise gets custom permissions and unlimited credits. -- **Flexible API referencing**: Users can reference APIs by either ID or name in subsequent operations, plus use scoped permissions like `api..verify_key` to restrict operations without specifying API identifiers in each request. -- **Credit-based billing**: Credits are deducted after security checks pass (not immediately), enabling consumption-based pricing where users are only charged for successful verifications. - -### Permission Documentation Standards -- **Use "one of the following" pattern**: Make it clear when users need either/or permissions rather than all permissions listed. -- **Include both wildcard and scoped options**: Document both `api.*.action` (all APIs) and `api..action` (specific API) permission patterns. -- **List permissions in descriptions**: Include required permissions directly in the description text rather than custom OpenAPI directives so they appear in rendered documentation. - -### Technical Implementation Details -- **verifyKey always returns HTTP 200**: Success/failure determined by `valid` field in response data, not HTTP status code. This prevents information leakage about key existence. -- **Response codes corrected**: Fixed inconsistent response codes like `USAGE_EXCEEDED` → `INSUFFICIENT_CREDITS` and `INSUFFICIENT_PERMISSIONS` → `FORBIDDEN` to match actual handler implementation. -- **Credits timing**: Credits are deducted after all security checks pass, ensuring users are only charged for successful verifications. - -### Documentation Philosophy Applied -- **Business context over technical details**: Each endpoint now explains not just how to use it, but when and why developers would use it in real-world scenarios. -- **Practical, tier-based examples**: Examples reflect actual usage patterns like free/paid/enterprise tiers rather than generic scenarios. -- **Clear error handling**: Documented all possible failure scenarios with specific response codes and when they occur. -- **Cross-references**: Connected related concepts like API creation → key creation → key verification workflow. -- **Avoid filler words**: Removed words like "comprehensive" that don't add value - focus on being direct and specific. - -## Next Steps - -Continue with the remaining 29 endpoints, following the same process: -1. Ask contextual questions about business use cases -2. Read handler and test files to understand implementation -3. Update OpenAPI documentation with comprehensive details -4. Ensure proper file organization (examples in schema files) -5. Use consistent formatting (multiline strings, readable IDs) -6. Move to next endpoint - -The goal is to make each endpoint's documentation comprehensive enough that developers can understand not just how to call it, but when to use it, what it does behind the scenes, and how it fits into their overall API key management strategy. - -## 🎉 TASK COMPLETED! - -**Final Status: 36 of 36 endpoints completed (100%)** - -All v2 API endpoints have been comprehensively documented with: -- Clear business context and use cases -- Proper permission requirements -- Detailed side effects documentation -- Comprehensive error handling examples -- Practical, real-world examples -- Consistent formatting and structure - -### Groups Completed: -- **APIs Management**: 4/4 endpoints ✅ COMPLETED -- **Key Management**: 6/6 endpoints ✅ COMPLETED -- **Permission Management**: 14/14 endpoints ✅ COMPLETED -- **Rate Limiting**: 5/5 endpoints ✅ COMPLETED -- **Identity Management**: 5/5 endpoints ✅ COMPLETED -- **System Health**: 1/1 endpoint ✅ COMPLETED - -The v2 API documentation overhaul is now complete and ready to drive both documentation pages and generated SDKs with comprehensive, developer-friendly content. \ No newline at end of file From eda888a2f082c913aaaf09509ec2dd53aa51d134 Mon Sep 17 00:00:00 2001 From: Andreas Thomas Date: Thu, 24 Jul 2025 18:13:41 +0200 Subject: [PATCH 7/9] Delete go/apps/api/openapi/openapi.yaml --- go/apps/api/openapi/openapi.yaml | 7390 ------------------------------ 1 file changed, 7390 deletions(-) delete mode 100644 go/apps/api/openapi/openapi.yaml diff --git a/go/apps/api/openapi/openapi.yaml b/go/apps/api/openapi/openapi.yaml deleted file mode 100644 index effe0c1e1b..0000000000 --- a/go/apps/api/openapi/openapi.yaml +++ /dev/null @@ -1,7390 +0,0 @@ -# yaml-language-server: $schema=https://json-schema.org/draft/2020-12/schema -info: - title: Unkey API - version: 2.0.0 - description: |- - Unkey's API provides programmatic access for all resources within our platform. - - - - ### Authentication - # - This API uses HTTP Bearer authentication with root keys. Most endpoints require specific permissions associated with your root key. When making requests, include your root key in the `Authorization` header: - ``` - Authorization: Bearer unkey_xxxxxxxxxxx - ``` - - All responses follow a consistent envelope structure that separates operational metadata from actual data. This design provides several benefits: - - Debugging: Every response includes a unique requestId for tracing issues - - Consistency: Predictable response format across all endpoints - - Extensibility: Easy to add new metadata without breaking existing integrations - - Error Handling: Unified error format with actionable information - - ### Success Response Format: - ```json - { - "meta": { - "requestId": "req_123456" - }, - "data": { - // Actual response data here - } - } - ``` - - The meta object contains operational information: - - `requestId`: Unique identifier for this request (essential for support) - - The data object contains the actual response data specific to each endpoint. - - ### Paginated Response Format: - ```json - { - "meta": { - "requestId": "req_123456" - }, - "data": [ - // Array of results - ], - "pagination": { - "cursor": "next_page_token", - "hasMore": true - } - } - ``` - - The pagination object appears on list endpoints and contains: - - `cursor`: Token for requesting the next page - - `hasMore`: Whether more results are available - - ### Error Response Format: - ```json - { - "meta": { - "requestId": "req_2c9a0jf23l4k567" - }, - "error": { - "detail": "The resource you are attempting to modify is protected and cannot be changed", - "status": 403, - "title": "Forbidden", - "type": "https://unkey.com/docs/api-reference/errors-v2/unkey/application/protected_resource" - } - } - ``` - - Error responses include comprehensive diagnostic information: - - `title`: Human-readable error summary - - `detail`: Specific description of what went wrong - - `status`: HTTP status code - - `type`: Link to error documentation - - `errors`: Array of validation errors (for 400 responses) - - This structure ensures you always have the context needed to debug issues and take corrective action. -openapi: 3.0.0 -servers: - - url: https://api.unkey.com -x-speakeasy-retries: - strategy: backoff - backoff: - initialInterval: 50 - maxInterval: 1000 - maxElapsedTime: 10000 - exponent: 1.5 - statusCodes: - - 5XX - retryConnectionErrors: true -security: - - rootKey: [] -components: - securitySchemes: - rootKey: - type: http - scheme: bearer - bearerFormat: root key - description: |- - Unkey uses API keys (root keys) for authentication. These keys authorize access to management operations in the API. - - To authenticate, include your root key in the Authorization header of each request: - ``` - Authorization: Bearer unkey_123 - ``` - - Root keys have specific permissions attached to them, controlling what operations they can perform. Key permissions follow a hierarchical structure with patterns like `resource.resource_id.action` (e.g., `apis.*.create_key`, `apis.*.read_api`). - - Security best practices: - - Keep root keys secure and never expose them in client-side code - - Use different root keys for different environments - - Rotate keys periodically, especially after team member departures - - Create keys with minimal necessary permissions following least privilege principle - - Monitor key usage with audit logs. - x-speakeasy-example: UNKEY_ROOT_KEY - schemas: - V2KeysUpdateKeyRequestBody: - type: object - required: - - keyId - properties: - keyId: - type: string - minLength: 3 - maxLength: 255 # Reasonable upper bound for database identifiers - pattern: "^[a-zA-Z0-9_]+$" - description: | - Specifies which key to update using the database identifier returned from `createKey`. - Do not confuse this with the actual API key string that users include in requests. - example: key_2cGKbMxRyIzhCxo1Idjz8q - name: - type: string - minLength: 1 - maxLength: 255 - nullable: true - description: | - Sets a human-readable name for internal organization and identification. - Omitting this field leaves the current name unchanged, while setting null removes it entirely. - Avoid generic names like "API Key" when managing multiple keys per user or service. - example: Payment Service Production Key - externalId: - type: string - nullable: true - minLength: 1 - maxLength: 255 - pattern: "^[a-zA-Z0-9_.-]+$" - description: | - Links this key to a user or entity in your system for ownership tracking during verification. - Omitting this field preserves the current association, while setting null disconnects the key from any identity. - Essential for user-specific analytics, billing, and key management across multiple users. - Supports letters, numbers, underscores, dots, and hyphens for flexible identifier formats. - example: user_912a841d - meta: - type: object - nullable: true - additionalProperties: true - maxProperties: 100 # Prevent DoS while allowing rich metadata - description: | - Stores arbitrary JSON metadata returned during key verification. - Omitting this field preserves existing metadata, while setting null removes all metadata entirely. - Avoid storing sensitive data here as it's returned in verification responses. - Large metadata objects increase verification latency and should stay under 10KB total size. - example: - plan: enterprise - limits: - storage: 500GB - compute: 1000 minutes/month - features: [analytics, exports, webhooks] - hasAcceptedTerms: true - billing: - cycle: monthly - next_billing: "2024-01-15" - preferences: - timezone: "UTC" - notifications: true - lastBillingDate: "2023-10-15" - expires: - type: integer - nullable: true - format: int64 - minimum: 0 - maximum: 4102444800000 # January 1, 2100 - reasonable future limit - description: | - Sets when this key automatically expires as a Unix timestamp in milliseconds. - Verification fails with code=EXPIRED immediately after this time passes. - Omitting this field preserves the current expiration, while setting null makes the key permanent. - - Avoid setting timestamps in the past as they immediately invalidate the key. - Keys expire based on server time, not client time, which prevents timezone-related issues. - Active sessions continue until their next verification attempt after expiry. - example: 1704067200000 - credits: - "$ref": "#/components/schemas/KeyCreditsData" - description: | - Controls usage-based limits for this key through credit consumption. - Omitting this field preserves current credit settings, while setting null enables unlimited usage. - Cannot configure refill settings when credits is null, and refillDay requires monthly interval. - Essential for implementing usage-based pricing and subscription quotas. - ratelimits: - type: array - maxItems: 50 # Reasonable limit for rate limit configurations per key - items: - "$ref": "#/components/schemas/RatelimitRequest" - description: | - Defines time-based rate limits that protect against abuse by controlling request frequency. - Omitting this field preserves existing rate limits, while setting null removes all rate limits. - Unlike credits which track total usage, rate limits reset automatically after each window expires. - Multiple rate limits can control different operation types with separate thresholds and windows. - enabled: - type: boolean - description: | - Controls whether the key is currently active for verification requests. - When set to `false`, all verification attempts fail with `code=DISABLED` regardless of other settings. - Omitting this field preserves the current enabled status. - Useful for temporarily suspending access during billing issues, security incidents, or maintenance windows without losing key configuration. - example: true - roles: - type: array - maxItems: 100 # Reasonable limit for role assignments per key - items: - type: string - minLength: 1 - maxLength: 100 # Keep role names concise and readable - pattern: "^[a-zA-Z][a-zA-Z0-9_-]*$" - description: | - Assigns existing roles to this key for permission management through role-based access control. - Roles must already exist in your workspace before assignment. - During verification, all permissions from assigned roles are checked against requested permissions. - Roles provide a convenient way to group permissions and apply consistent access patterns across multiple keys. - example: - - api_admin - - billing_reader - permissions: - type: array - maxItems: 1000 # Allow extensive permission sets for complex applications - items: - type: string - minLength: 1 - maxLength: 100 # Keep permission names concise and readable - pattern: "^[a-zA-Z0-9_]+$" - description: | - Grants specific permissions directly to this key without requiring role membership. - Wildcard permissions like `documents.*` grant access to all sub-permissions including `documents.read` and `documents.write`. - Direct permissions supplement any permissions inherited from assigned roles. - example: - - documents.read - - documents.write - - settings.view - additionalProperties: false - KeysUpdateKeyResponseData: - type: object - properties: {} - additionalProperties: false - description: Empty response object by design. A successful response indicates the key was updated successfully. - V2KeysUpdateKeyResponseBody: - type: object - required: - - meta - properties: - meta: - "$ref": "#/components/schemas/Meta" - data: - "$ref": "#/components/schemas/KeysUpdateKeyResponseData" - V2KeysDeleteKeyRequestBody: - type: object - required: - - keyId - properties: - keyId: - type: string - minLength: 3 - maxLength: 255 # Reasonable upper bound for database identifiers - pattern: "^[a-zA-Z0-9_]+$" - description: | - Specifies which key to delete using the database identifier returned from `createKey`. - Do not confuse this with the actual API key string that users include in requests. - Deletion immediately invalidates the key, causing all future verification attempts to fail with `code=NOT_FOUND`. - Key deletion triggers cache invalidation across all regions but may take up to 30 seconds to fully propagate. - example: key_2cGKbMxRyIzhCxo1Idjz8q - permanent: - type: boolean - default: false - description: | - Controls deletion behavior between recoverable soft-deletion and irreversible permanent erasure. - Soft deletion (default) preserves key data for potential recovery through direct database operations. - Permanent deletion completely removes all traces including hash values and metadata with no recovery option. - - Use permanent deletion only for regulatory compliance (GDPR), resolving hash collisions, or when reusing identical key strings. - Permanent deletion cannot be undone and may affect analytics data that references the deleted key. - Most applications should use soft deletion to maintain audit trails and prevent accidental data loss. - example: false - additionalProperties: false - KeysDeleteKeyResponseData: - type: object - properties: {} - additionalProperties: false - description: Empty response object by design. A successful response indicates the key was deleted successfully. - V2KeysDeleteKeyResponseBody: - type: object - required: - - meta - properties: - meta: - "$ref": "#/components/schemas/Meta" - data: - "$ref": "#/components/schemas/KeysDeleteKeyResponseData" - V2KeysGetKeyRequestBody: - type: object - properties: - keyId: - type: string - minLength: 3 - maxLength: 255 # Reasonable upper bound for database identifiers - pattern: "^[a-zA-Z0-9_]+$" - description: | - Specifies which key to retrieve using the database identifier returned from `keys.createKey`. - Do not confuse this with the actual API key string that users include in requests. - Key data includes metadata, permissions, usage statistics, and configuration but never the plaintext key value unless `decrypt=true`. - Find this ID in creation responses, key listings, dashboard, or verification responses. - example: key_2cGKbMxRyIzhCxo1Idjz8q - decrypt: - type: boolean - default: false - description: | - Controls whether to include the plaintext key value in the response for recovery purposes. - Only works for keys created with `recoverable=true` and requires the `decrypt_key` permission. - Returned keys must be handled securely, never logged, cached, or stored insecurely. - - Use only for legitimate recovery scenarios like user password resets or emergency access. - Most applications should keep this false to maintain security best practices and avoid accidental key exposure. - Decryption requests are audited and may trigger security alerts in enterprise environments. - key: - type: string - minLength: 1 - maxLength: 512 # Reasonable upper bound for API key strings - description: | - The complete API key string provided by you, including any prefix. - Never log, cache, or store API keys in your system as they provide full access to user resources. - Include the full key exactly as provided - even minor modifications will cause a not found error. - example: prefix_f4cc2d765275c206b7d76ff0e92e583067c4e33603fb4055d7ba3031cd7ce36a - additionalProperties: false - oneOf: - - required: - - keyId - - required: - - key - V2KeysGetKeyResponseBody: - type: object - required: - - meta - - data - properties: - meta: - "$ref": "#/components/schemas/Meta" - data: - "$ref": "#/components/schemas/KeyResponseData" - V2KeysVerifyKeyResponseBody: - type: object - required: - - meta - - data - properties: - meta: - "$ref": "#/components/schemas/Meta" - data: - "$ref": "#/components/schemas/KeysVerifyKeyResponseData" - V2KeysAddPermissionsRequestBody: - type: object - required: - - keyId - - permissions - properties: - keyId: - type: string - minLength: 3 - maxLength: 255 # Reasonable upper bound for database identifiers - pattern: "^[a-zA-Z0-9_]+$" - description: | - Specifies which key receives the additional permissions using the database identifier returned from `keys.createKey`. - Do not confuse this with the actual API key string that users include in requests. - Added permissions supplement existing permissions and roles without replacing them. - Permission changes take effect immediately but may take up to 30 seconds to propagate across all regions. - example: key_2cGKbMxRyIzhCxo1Idjz8q - permissions: - type: array - minItems: 1 - maxItems: 1000 # Allow extensive permission sets for complex applications - description: | - Grants additional permissions to the key through direct assignment or automatic creation. - Duplicate permissions are ignored automatically, making this operation idempotent. - Use either ID for existing permissions or slug for new permissions with optional auto-creation. - - Permission changes take effect immediately but cache propagation across regions may take up to 30 seconds. - Adding permissions never removes existing permissions or role-based permissions. - items: - type: object - properties: - id: - type: string - minLength: 3 - maxLength: 255 # Reasonable upper bound for database identifiers - pattern: "^[a-zA-Z0-9_]+$" - description: | - References an existing permission by its database identifier. - Use when you know the exact permission ID and want to ensure you're referencing a specific permission. - Takes precedence over slug when both are provided in the same object. - The referenced permission must already exist in your workspace. - example: perm_1n9McEIBSqy44Qy7hzWyM5 - slug: - type: string - minLength: 1 - maxLength: 100 # Keep permission names concise and readable - pattern: "^[a-zA-Z0-9_.]+$" - description: | - Identifies the permission by its human-readable name using hierarchical naming patterns. - Use `resource.action` format for logical organization and verification flexibility. - Slugs must be unique within your `workspace` and support wildcard matching during verification. - Combined with `create=true`, allows automatic permission creation for streamlined workflows. - example: documents.write - create: - type: boolean - default: false - description: | - Enables automatic permission creation when the specified slug does not exist. - Only works with slug-based references, not ID-based references. - Requires the `rbac.*.create_permission` permission on your root key. - - Created permissions are permanent and visible workspace-wide to all API keys. - Use carefully to avoid permission proliferation from typos or uncontrolled creation. - Consider centralizing permission creation in controlled processes for better governance. - Auto-created permissions use the slug as both the name and identifier. - additionalProperties: false - additionalProperties: false - V2KeysAddPermissionsResponseData: - type: array - description: |- - Complete list of all permissions directly assigned to the key (including both newly added permissions and those that were already assigned). - - This response includes: - - All direct permissions assigned to the key (both pre-existing and newly added) - - Permissions sorted alphabetically by name for consistent response format - - Both the permission ID and name for each permission - - Important notes: - - This list does NOT include permissions granted through roles - - For a complete permission picture, use `/v2/keys.getKey` instead - - An empty array indicates the key has no direct permissions assigned - items: - type: object - required: - - id - - name - - slug - properties: - id: - type: string - description: - The unique identifier of the permission (begins with `perm_`). - This ID can be used in other API calls to reference this specific permission. - IDs are guaranteed unique and won't change, making them ideal for scripting - and automation. You can store these IDs in your system for consistent - reference. - example: perm_1n9McEIBSqy44Qy7hzWyM5 - name: - type: string - description: "The human-readable name of the permission. " - example: Can write documents - slug: - type: string - description: |- - The slug of the permission, typically following a `resource.action` pattern like `documents.read`. - example: documents.write - V2KeysAddPermissionsResponse: - type: object - required: - - meta - - data - properties: - meta: - "$ref": "#/components/schemas/Meta" - data: - "$ref": "#/components/schemas/V2KeysAddPermissionsResponseData" - V2KeysRemovePermissionsRequestBody: - type: object - required: - - keyId - - permissions - properties: - keyId: - type: string - minLength: 3 - maxLength: 255 # Reasonable upper bound for database identifiers - pattern: "^[a-zA-Z0-9_]+$" - description: | - Specifies which key to remove permissions from using the database identifier returned from `keys.createKey`. - Do not confuse this with the actual API key string that users include in requests. - Removing permissions only affects direct assignments, not permissions inherited from roles. - Permission changes take effect immediately but may take up to 30 seconds to propagate across all regions. - example: key_2cGKbMxRyIzhCxo1Idjz8q - permissions: - type: array - minItems: 1 - maxItems: 1000 # Allow extensive permission sets for complex applications - description: | - Removes direct permissions from the key without affecting role-based permissions. - Operations are idempotent - removing non-existent permissions has no effect and causes no errors. - Use either ID for existing permissions or name for exact string matching. - - After removal, verification checks for these permissions will fail unless granted through roles. - Permission changes take effect immediately but cache propagation across regions may take up to 30 seconds. - Removing all direct permissions does not disable the key, only removes its direct permission grants. - items: - type: object - properties: - id: - type: string - minLength: 3 - maxLength: 255 # Reasonable upper bound for database identifiers - pattern: "^[a-zA-Z0-9_]+$" - description: | - References the permission to remove by its database identifier. - Use when you know the exact permission ID and want to ensure you're removing a specific permission. - Takes precedence over name when both are provided in the same object. - Essential for automation scripts where precision prevents accidental permission removal. - example: perm_1n9McEIBSqy44Qy7hzWyM5 - slug: - type: string - minLength: 1 - maxLength: 100 # Keep permission slugs concise and readable - pattern: "^[a-zA-Z0-9_.]+$" - description: | - Identifies the permission by slug for removal from the key's direct assignment list. - example: documents.write - additionalProperties: false - additionalProperties: false - V2KeysRemovePermissionsResponseData: - type: array - description: |- - Complete list of all permissions directly assigned to the key after the removal operation (remaining permissions only). - - This response includes: - - All direct permissions still assigned to the key after removal - - Permissions sorted alphabetically by name for consistent response format - - Both the permission ID and name for each remaining permission - - Important notes: - - This list does NOT include permissions granted through roles - - For a complete permission picture, use `/v2/keys.getKey` instead - - An empty array indicates the key has no direct permissions assigned - - Any cached versions of the key are immediately invalidated to ensure consistency - - Changes to permissions take effect within seconds for new verifications - - All permission removals are logged to the audit log for security tracking - items: - type: object - required: - - id - - name - - slug - properties: - id: - type: string - description: - The unique identifier of the permission (begins with `perm_`). - This ID can be used in other API calls to reference this specific permission. - IDs are guaranteed unique and won't change, making them ideal for scripting - and automation. You can store these IDs in your system for consistent - reference. - example: perm_1n9McEIBSqy44Qy7hzWyM5 - name: - type: string - description: The name of the permission - example: documents.write - slug: - type: string - description: |- - The slug of the permission, typically following a `resource.action` pattern like `documents.read`. Names are human-readable identifiers used both for assignment and verification. - - During verification: - - The exact name is matched (e.g., `documents.read`) - - Hierarchical wildcards are supported in verification requests - - A key with permission `documents.*` grants access to `documents.read`, `documents.write`, etc. - - Wildcards can appear at any level: `billing.*.view` matches `billing.invoices.view` and `billing.payments.view` - - However, when adding permissions, you must specify each exact permission - wildcards are not valid for assignment. - example: documents.write - V2KeysRemovePermissionsResponse: - type: object - required: - - meta - - data - properties: - meta: - "$ref": "#/components/schemas/Meta" - data: - "$ref": "#/components/schemas/V2KeysRemovePermissionsResponseData" - V2KeysSetPermissionsRequestBody: - type: object - required: - - keyId - - permissions - properties: - keyId: - type: string - description: - The unique identifier of the key to set permissions on (begins - with 'key_'). This ID comes from the createKey response and identifies - which key will have its permissions replaced. This is the database ID, - not the actual API key string that users authenticate with. - example: key_2cGKbMxRyIzhCxo1Idjz8q - minLength: 3 - permissions: - type: array - description: |- - The permissions to set for this key. This is a complete replacement operation - it overwrites all existing direct permissions with this new set. - - Key behaviors: - - Providing an empty array removes all direct permissions from the key - - This only affects direct permissions - permissions granted through roles are not affected - - All existing direct permissions not included in this list will be removed - - The complete list approach allows synchronizing permissions with external systems - - Permission changes take effect immediately for new verifications - - Unlike addPermissions (which only adds) or removePermissions (which only removes), this endpoint performs a wholesale replacement of the permission set. - items: - type: object - properties: - id: - type: string - description: - The ID of an existing permission (begins with `perm_`). - Provide either ID or slug for each permission, not both. Using ID - is more precise and guarantees you're referencing the exact permission - intended, regardless of slug changes or duplicates. IDs are particularly - useful in automation scripts and when migrating permissions between - environments. - example: perm_1n9McEIBSqy44Qy7hzWyM5 - minLength: 3 - slug: - type: string - description: - The slug of the permission. Provide either ID or slug - for each permission, not both. Slugs must match exactly as defined - in your permission system - including case sensitivity and the complete - hierarchical path. Slugs are generally more human-readable but can - be ambiguous if not carefully managed across your workspace. - example: documents.write - minLength: 1 - create: - type: boolean - description: |- - When true, if a permission with this slug doesn't exist, it will be automatically created on-the-fly. Only works when specifying slug, not ID. - - SECURITY CONSIDERATIONS: - - Requires the `rbac.*.create_permission` permission on your root key - - Created permissions are permanent and visible throughout your workspace - - Use carefully to avoid permission proliferation and inconsistency - - Consider using a controlled process for permission creation instead - - Typos with `create=true` will create unintended permissions that persist in your system - default: false - additionalProperties: false - additionalProperties: false - V2KeysSetPermissionsResponseData: - type: array - description: |- - Complete list of all permissions now directly assigned to the key after the set operation has completed. - - The response includes: - - The comprehensive, updated set of direct permissions (reflecting the complete replacement) - - Both ID and name for each permission for easy reference - - Permissions sorted alphabetically by name for consistent response format - - Important notes: - - This only shows direct permissions, not those granted through roles - - An empty array means the key has no direct permissions assigned - - For a complete permission picture including roles, use keys.getKey instead - - All permission changes are logged in the audit log for security tracking - items: - type: object - required: - - id - - name - properties: - id: - type: string - description: The unique identifier of the permission - example: perm_1n9McEIBSqy44Qy7hzWyM5 - name: - type: string - description: The name of the permission - example: documents.write - V2KeysSetPermissionsResponse: - type: object - required: - - meta - - data - properties: - meta: - "$ref": "#/components/schemas/Meta" - data: - "$ref": "#/components/schemas/V2KeysSetPermissionsResponseData" - V2KeysAddRolesRequestBody: - type: object - required: - - keyId - - roles - properties: - keyId: - type: string - minLength: 3 - maxLength: 255 # Reasonable upper bound for database identifiers - pattern: "^[a-zA-Z0-9_]+$" - description: | - Specifies which key receives the additional roles using the database identifier returned from `createKey`. - Do not confuse this with the actual API key string that users include in requests. - Added roles supplement existing roles and permissions without replacing them. - Role assignments take effect immediately but may take up to 30 seconds to propagate across all regions. - example: key_2cGKbMxRyIzhCxo1Idjz8q - roles: - type: array - minItems: 1 - maxItems: 100 # Reasonable limit for role assignments per key - description: | - Assigns additional roles to the key through direct assignment to existing workspace roles. - Operations are idempotent - adding existing roles has no effect and causes no errors. - Use either ID for existing roles or name for human-readable references. - - All roles must already exist in the workspace - roles cannot be created automatically. - Invalid roles cause the entire operation to fail atomically, ensuring consistent state. - Role assignments take effect immediately but cache propagation across regions may take up to 30 seconds. - items: - type: object - properties: - id: - type: string - minLength: 3 - maxLength: 255 # Reasonable upper bound for database identifiers - pattern: "^[a-zA-Z0-9_]+$" - description: | - References an existing role by its database identifier. - Use when you know the exact role ID and want to ensure you're referencing a specific role. - Takes precedence over name when both are provided in the same object. - Essential for automation scripts where role names might change but IDs remain stable. - example: role_1n9McEIBSqy44Qy7hzWyM5 - name: - type: string - minLength: 1 - maxLength: 100 # Keep role names concise and readable - pattern: "^[a-zA-Z][a-zA-Z0-9_-]*$" - description: | - Identifies the role by its human-readable name within the workspace. - Role names must start with a letter and contain only letters, numbers, underscores, or hyphens. - Names must be unique within the workspace and are case-sensitive. - More readable than IDs but vulnerable to integration breaks if roles are renamed. - Use IDs for automation and names for human-configured integrations. - example: admin - additionalProperties: false - additionalProperties: false - V2KeysAddRolesResponseData: - type: array - description: |- - Complete list of all roles directly assigned to the key after the operation completes. - - The response includes: - - All roles now assigned to the key (both pre-existing and newly added) - - Both ID and name of each role for easy reference - - Roles sorted alphabetically by name for consistent response format - - Important notes: - - The response shows the complete current state after the addition - - An empty array means the key has no roles assigned (unlikely after an add operation) - - This only shows direct role assignments, not inherited or nested roles - - Role permissions are not expanded in this response - use keys.getKey for full details - - All role changes are logged in the audit log for security tracking - items: - type: object - required: - - id - - name - properties: - id: - type: string - description: - The unique identifier of the role (begins with `role_`). - This ID can be used in other API calls to reference this specific role. - Role IDs are immutable and guaranteed to be unique within your Unkey - workspace, making them reliable reference points for integration and - automation systems. - example: role_1n9McEIBSqy44Qy7hzWyM5 - name: - type: string - description: - The name of the role. This is a human-readable identifier - that's unique within your workspace. Role names help identify what access - level or function a role provides. Common patterns include naming by - access level (`admin`, `editor`, `viewer`), by department (`billing_manager`, - `support_agent`), or by feature area (`analytics_user`, `dashboard_admin`). - example: admin - V2KeysAddRolesResponse: - type: object - required: - - meta - - data - properties: - meta: - "$ref": "#/components/schemas/Meta" - data: - "$ref": "#/components/schemas/V2KeysAddRolesResponseData" - V2KeysRemoveRolesRequestBody: - type: object - required: - - keyId - - roles - properties: - keyId: - type: string - minLength: 3 - maxLength: 255 # Reasonable upper bound for database identifiers - pattern: "^[a-zA-Z0-9_]+$" - description: | - Specifies which key loses the roles using the database identifier returned from createKey. - Do not confuse this with the actual API key string that users include in requests. - Removing roles only affects direct assignments, not permissions inherited from other sources. - Role changes take effect immediately but may take up to 30 seconds to propagate across all regions. - example: key_2cGKbMxRyIzhCxo1Idjz8q - roles: - type: array - minItems: 1 - maxItems: 100 # Reasonable limit for role assignments per key - description: | - Removes direct role assignments from the key without affecting other role sources or permissions. - Operations are idempotent - removing non-assigned roles has no effect and causes no errors. - Use either ID for existing roles or name for exact string matching. - - After removal, the key loses access to permissions that were only granted through these roles. - Role changes take effect immediately but cache propagation across regions may take up to 30 seconds. - Invalid role references cause the entire operation to fail atomically, ensuring consistent state. - items: - type: object - properties: - id: - type: string - minLength: 3 - maxLength: 255 # Reasonable upper bound for database identifiers - pattern: "^[a-zA-Z0-9_]+$" - description: | - References the role to remove by its database identifier. - Use when you know the exact role ID and want to ensure you're removing a specific role. - Takes precedence over name when both are provided in the same object. - Essential for automation scripts where role names might change but IDs remain stable. - example: role_1n9McEIBSqy44Qy7hzWyM5 - name: - type: string - minLength: 1 - maxLength: 100 # Keep role names concise and readable - pattern: "^[a-zA-Z][a-zA-Z0-9_-]*$" - description: | - Identifies the role to remove by its exact name with case-sensitive matching. - Must match the complete role name as currently defined in the workspace, starting with a letter and using only letters, numbers, underscores, or hyphens. - More readable than IDs but vulnerable to integration breaks if roles are renamed. - Use IDs for automation and names for human-configured integrations. - example: admin - additionalProperties: false - additionalProperties: false - V2KeysRemoveRolesResponseData: - type: array - description: |- - Complete list of all roles directly assigned to the key after the removal operation completes. - - The response includes: - - The remaining roles still assigned to the key (after removing the specified roles) - - Both ID and name for each role for easy reference - - Roles sorted alphabetically by name for consistent response format - - Important notes: - - The response reflects the current state after the removal operation - - An empty array indicates the key now has no roles assigned - - This only shows direct role assignments - - Role permissions are not expanded in this response - use keys.getKey for full details - - All role changes are logged in the audit log for security tracking - - Changes take effect immediately for new verifications but cached sessions may retain old permissions briefly - items: - type: object - required: - - id - - name - properties: - id: - type: string - description: - The unique identifier of the role (begins with `role_`). - This ID can be used in other API calls to reference this specific role. - example: role_1n9McEIBSqy44Qy7hzWyM5 - name: - type: string - description: - The name of the role. This is a human-readable identifier - that's unique within your workspace. - example: admin - V2KeysRemoveRolesResponse: - type: object - required: - - meta - - data - properties: - meta: - "$ref": "#/components/schemas/Meta" - data: - "$ref": "#/components/schemas/V2KeysRemoveRolesResponseData" - V2KeysSetRolesRequestBody: - type: object - required: - - keyId - - roles - properties: - keyId: - type: string - minLength: 3 - maxLength: 255 # Reasonable upper bound for database identifiers - pattern: "^[a-zA-Z0-9_]+$" - description: | - Specifies which key gets the complete role replacement using the database identifier returned from createKey. - Do not confuse this with the actual API key string that users include in requests. - This is a wholesale replacement operation that removes all existing roles not included in the request. - Role changes take effect immediately but may take up to 30 seconds to propagate across all regions. - example: key_2cGKbMxRyIzhCxo1Idjz8q - roles: - type: array - maxItems: 100 # Reasonable limit for role assignments per key - description: | - Replaces all existing role assignments with this complete list of roles. - This is a wholesale replacement operation, not an incremental update like add/remove operations. - Use either ID for existing roles or name for human-readable references. - - Providing an empty array removes all direct role assignments from the key. - All roles must already exist in the workspace - roles cannot be created automatically. - Invalid role references cause the entire operation to fail atomically, ensuring consistent state. - Role changes take effect immediately but cache propagation across regions may take up to 30 seconds. - items: - type: object - properties: - id: - type: string - minLength: 3 - maxLength: 255 # Reasonable upper bound for database identifiers - pattern: "^[a-zA-Z0-9_]+$" - description: | - References an existing role by its database identifier. - Use when you know the exact role ID and want to ensure you're referencing a specific role. - Takes precedence over name when both are provided in the same object. - Essential for automation scripts where role names might change but IDs remain stable. - example: role_1n9McEIBSqy44Qy7hzWyM5 - name: - type: string - minLength: 1 - maxLength: 100 # Keep role names concise and readable - pattern: "^[a-zA-Z][a-zA-Z0-9_-]*$" - description: | - Identifies the role by its human-readable name within the workspace. - Role names must start with a letter and contain only letters, numbers, underscores, or hyphens. - Names must be unique within the workspace and are case-sensitive. - More readable than IDs but vulnerable to integration breaks if roles are renamed. - Use IDs for automation and names for human-configured integrations. - example: admin - additionalProperties: false - additionalProperties: false - V2KeysSetRolesResponseData: - type: array - description: |- - Complete list of all roles now directly assigned to the key after the set operation has completed. - - The response includes: - - The comprehensive, updated set of roles (reflecting the complete replacement) - - Both ID and name for each role for easy reference - - Roles sorted alphabetically by name for consistent response format - - Important notes: - - This response shows the final state after the complete replacement - - If you provided an empty array in the request, this will also be empty - - This only shows direct role assignments on the key - - Role permissions are not expanded in this response - use keys.getKey for complete details - - All role changes are logged in the audit log for security tracking - - An empty array indicates the key now has no roles assigned at all - items: - type: object - required: - - id - - name - properties: - id: - type: string - description: - The unique identifier of the role (begins with `role_`). - This ID can be used in other API calls to reference this specific role. - Role IDs are immutable and guaranteed to be unique, making them reliable - reference points for integration and automation systems. - example: role_1n9McEIBSqy44Qy7hzWyM5 - name: - type: string - description: - The name of the role. This is a human-readable identifier - that's unique within your workspace. Role names are descriptive labels - that help identify what access level or function a role provides. Good - naming practices include naming by access level ('admin', 'editor'), - by department ('billing_team', 'support_staff'), or by feature area - ('reporting_user', 'settings_manager'). - example: admin - V2KeysSetRolesResponse: - type: object - required: - - meta - - data - properties: - meta: - "$ref": "#/components/schemas/Meta" - data: - "$ref": "#/components/schemas/V2KeysSetRolesResponseData" - V2KeysUpdateCreditsRequestBody: - type: object - required: - - keyId - - operation - properties: - keyId: - type: string - description: - The ID of the key to update (begins with `key_`). This is the - database reference ID for the key, not the actual API key string that - users authenticate with. This ID uniquely identifies which key's credits - will be updated. - example: key_2cGKbMxRyIzhCxo1Idjz8q - minLength: 3 - value: - type: integer - format: int64 - nullable: true - minimum: 0 - maximum: 9223372036854775807 # Max int64 value for future-proofing - description: | - The new value for the remaining credits. This is an absolute value replacement, not an increment or decrement operation. - - Key behaviors: - - This completely replaces the current remaining credits value when operation is set to 'set' - - To add credits, either replace the current value with the new value or increment the current value by a new value - - To make a key unlimited, set value = null - - To make a key with unlimited usage have a specific limit, set remaining to a positive number - - If a decrement would result in a negative value, the remaining credits are set to zero - - Credits are decremented each time the key is successfully verified (by the cost value, default 1) - - When credits reach zero, verification fails with code=USAGE_EXCEEDED - - This field is useful for implementing usage-based pricing, subscription tiers, trial periods, or consumption quotas. - example: 1000 - operation: - type: string - enum: - - set - - increment - - decrement - description: | - The operation to perform on the remaining credits. This can be one of: - - set: Replace the current remaining credits value with the specified value. - - increment: Add the specified value to the current remaining credits value. - - decrement: Subtract the specified value from the current remaining credits value. - additionalProperties: false - V2KeysUpdateCreditsResponse: - type: object - required: - - meta - - data - properties: - meta: - "$ref": "#/components/schemas/Meta" - data: - "$ref": "#/components/schemas/KeyCreditsData" - V2KeysCreateKeyRequestBody: - type: object - required: - - apiId - properties: - apiId: - type: string - minLength: 3 - maxLength: 255 # Reasonable upper bound for database identifiers - pattern: "^[a-zA-Z0-9_]+$" - description: | - Specifies which API this key belongs to, providing complete isolation between environments. - Keys from one API cannot be used to access another API, preventing cross-environment access. - Create separate APIs for different environments (development, staging, production) and services. - example: api_2cGKbMxRjIzhCxo1IdjH3a - prefix: - type: string - minLength: 1 - maxLength: 16 # Keep prefixes short for readability - pattern: "^[a-zA-Z0-9_]+$" - description: | - Adds a visual identifier to the beginning of the generated key for easier recognition in logs and dashboards. - The prefix becomes part of the actual key string (e.g., `prod_xxxxxxxxx`). - Avoid using sensitive information in prefixes as they may appear in logs and error messages. - example: prod - name: - type: string - minLength: 1 - maxLength: 200 # Human-readable names should be concise but descriptive - description: | - Sets a human-readable identifier for internal organization and dashboard display. - Never exposed to end users, only visible in management interfaces and API responses. - Avoid generic names like "API Key" when managing multiple keys for the same user or service. - example: Payment Service Production Key - byteLength: - type: integer - minimum: 16 # Minimum secure key length - maximum: 255 # Reasonable upper bound for key generation - default: 16 - description: | - Controls the cryptographic strength of the generated key in bytes. - Higher values increase security but result in longer keys that may be harder to handle. - The default 16 bytes provides 2^128 possible combinations, sufficient for most applications. - Consider 32 bytes for highly sensitive APIs, but avoid values above 64 bytes unless specifically required. - example: 24 - externalId: - type: string - minLength: 1 - maxLength: 255 # Match database varchar limits for external identifiers - pattern: "^[a-zA-Z0-9_.-]+$" - description: | - Links this key to a user or entity in your system using your own identifier. - Returned during verification to identify the key owner without additional database lookups. - Essential for user-specific analytics, billing, and multi-tenant key management. - Use your primary user ID, organization ID, or tenant ID for best results. - Accepts letters, numbers, underscores, dots, and hyphens for flexible identifier formats. - example: user_912a841d - meta: - type: object - additionalProperties: true - maxProperties: 100 # Prevent DoS while allowing rich metadata - description: | - Stores arbitrary JSON metadata returned during key verification for contextual information. - Eliminates additional database lookups during verification, improving performance for stateless services. - Avoid storing sensitive data here as it's returned in verification responses. - Large metadata objects increase verification latency and should stay under 10KB total size. - example: - plan: enterprise - featureFlags: - betaAccess: true - concurrentConnections: 10 - customerName: Acme Corp - billing: - tier: premium - renewal: "2024-12-31" - roles: - type: array - maxItems: 100 # Reasonable limit for role assignments per key - items: - type: string - minLength: 1 - maxLength: 100 # Keep role names concise and readable - pattern: "^[a-zA-Z][a-zA-Z0-9_-]*$" - description: | - Assigns existing roles to this key for permission management through role-based access control. - Roles must already exist in your workspace before assignment. - During verification, all permissions from assigned roles are checked against requested permissions. - Roles provide a convenient way to group permissions and apply consistent access patterns across multiple keys. - example: - - api_admin - - billing_reader - permissions: - type: array - maxItems: 1000 # Allow extensive permission sets for complex applications - items: - type: string - minLength: 1 - maxLength: 100 # Keep permission names concise and readable - pattern: "^[a-zA-Z0-9_]+$" - description: | - Grants specific permissions directly to this key without requiring role membership. - Wildcard permissions like `documents.*` grant access to all sub-permissions including `documents.read` and `documents.write`. - Direct permissions supplement any permissions inherited from assigned roles. - example: - - documents.read - - documents.write - - settings.view - expires: - type: integer - format: int64 - minimum: 0 - maximum: 4102444800000 # January 1, 2100 - reasonable future limit - description: | - Sets when this key automatically expires as a Unix timestamp in milliseconds. - Verification fails with code=EXPIRED immediately after this time passes. - Omitting this field creates a permanent key that never expires. - - Avoid setting timestamps in the past as they immediately invalidate the key. - Keys expire based on server time, not client time, which prevents timezone-related issues. - Essential for trial periods, temporary access, and security compliance requiring key rotation. - example: 1704067200000 - credits: - "$ref": "#/components/schemas/KeyCreditsData" - description: | - Controls usage-based limits through credit consumption with optional automatic refills. - Unlike rate limits which control frequency, credits control total usage with global consistency. - Essential for implementing usage-based pricing, subscription tiers, and hard usage quotas. - Omitting this field creates unlimited usage, while setting null is not allowed during creation. - ratelimits: - type: array - maxItems: 50 # Reasonable limit for rate limit configurations per identity - items: - "$ref": "#/components/schemas/RatelimitRequest" - description: | - Defines time-based rate limits that protect against abuse by controlling request frequency. - Unlike credits which track total usage, rate limits reset automatically after each window expires. - Multiple rate limits can control different operation types with separate thresholds and windows. - Essential for preventing API abuse while maintaining good performance for legitimate usage. - example: - - name: requests - limit: 100 - duration: 60000 - - name: heavy_operations - limit: 10 - duration: 3600000 - enabled: - type: boolean - default: true - description: | - Controls whether the key is active immediately upon creation. - When set to `false`, the key exists but all verification attempts fail with `code=DISABLED`. - Useful for pre-creating keys that will be activated later or for keys requiring manual approval. - Most keys should be created with `enabled=true` for immediate use. - example: true - recoverable: - type: boolean - default: false - description: | - Controls whether the plaintext key is stored in an encrypted vault for later retrieval. - When true, allows recovering the actual key value using keys.getKey with decrypt=true. - When false, the key value cannot be retrieved after creation for maximum security. - Only enable for development keys or when key recovery is absolutely necessary. - example: false - additionalProperties: false - KeysCreateKeyResponseData: - type: object - properties: - keyId: - type: string - description: - The unique identifier for this key in Unkey's system. This - is NOT the actual API key, but a reference ID used for management operations - like updating or deleting the key. Store this ID in your database to reference - the key later. This ID is not sensitive and can be logged or displayed - in dashboards. - example: key_2cGKbMxRyIzhCxo1Idjz8q - key: - type: string - description: - "The full generated API key that should be securely provided - to your user. SECURITY WARNING: This is the only time you'll receive - the complete key - Unkey only stores a securely hashed version. Never - log or store this value in your own systems; provide it directly to your - end user via secure channels. After this API call completes, this value - cannot be retrieved again (unless created with `recoverable=true`)." - example: prod_2cGKbMxRjIzhCxo1IdjH3arELti7Sdyc8w6XYbvtcyuBowPT - required: - - keyId - - key - V2KeysCreateKeyResponseBody: - type: object - required: - - meta - - data - properties: - meta: - "$ref": "#/components/schemas/Meta" - data: - "$ref": "#/components/schemas/KeysCreateKeyResponseData" - Meta: - type: object - required: - - requestId - properties: - requestId: - description: - A unique id for this request. Always include this ID when contacting - support about a specific API request. This identifier allows Unkey's support - team to trace the exact request through logs and diagnostic systems to - provide faster assistance. - example: req_123 - type: string - additionalProperties: false - description: - Metadata object included in every API response. This provides context - about the request and is essential for debugging, audit trails, and support - inquiries. The `requestId` is particularly important when troubleshooting issues - with the Unkey support team. - Pagination: - type: object - properties: - cursor: - type: string - minLength: 1 - maxLength: 1024 # Reasonable upper bound for cursor tokens - description: | - Opaque pagination token for retrieving the next page of results. - Include this exact value in the cursor field of subsequent requests. - Cursors are temporary and may expire after extended periods. - example: eyJrZXkiOiJrZXlfMTIzNCIsInRzIjoxNjk5Mzc4ODAwfQ== - hasMore: - type: boolean - description: | - Indicates whether additional results exist beyond this page. - When true, use the cursor to fetch the next page. - When false, you have reached the end of the result set. - example: true - required: - - hasMore - additionalProperties: false - BaseError: - properties: - detail: - description: - A human-readable explanation specific to this occurrence of - the problem. This provides detailed information about what went wrong - and potential remediation steps. The message is intended to be helpful - for developers troubleshooting the issue. - example: Property foo is required but is missing. - type: string - status: - description: - HTTP status code that corresponds to this error. This will - match the status code in the HTTP response. Common codes include `400` (Bad - Request), `401` (Unauthorized), `403` (Forbidden), `404` (Not Found), `409` (Conflict), - and `500` (Internal Server Error). - example: 404 - format: int - type: integer - title: - description: - A short, human-readable summary of the problem type. This is - a concise, fixed string that categorizes the error and remains consistent - between occurrences of the same error type. It provides a quick way to - identify the category of error. - type: string - example: Not Found - type: - description: - A URI reference to human-readable documentation for the error. - This link points to detailed documentation about this specific error type, - including possible causes and solutions. It's designed to help developers - understand and resolve the issue. - example: https://unkey.dev/errors/not-found - format: uri - type: string - type: object - required: - - detail - - status - - title - - type - description: - Standard error structure that provides detailed information about - errors encountered during API operations. This follows a problem details format - that includes both machine-readable error codes and human-readable explanations. - All error responses in the API include this structure to provide consistent, - actionable error information. - ValidationError: - additionalProperties: false - properties: - location: - description: |- - JSON path indicating exactly where in the request the error occurred. This helps pinpoint the problematic field or parameter. Examples include: - - 'body.name' (field in request body) - - 'body.items[3].tags' (nested array element) - - 'path.apiId' (path parameter) - - 'query.limit' (query parameter) - - Use this location to identify exactly which part of your request needs correction. - type: string - example: body.permissions[0].name - message: - description: - Detailed error message explaining what validation rule was - violated. This provides specific information about why the field or parameter - was rejected, such as format errors, invalid values, or constraint violations. - type: string - example: Must be at least 3 characters long - fix: - description: - A human-readable suggestion describing how to fix the error. - This provides practical guidance on what changes would satisfy the validation - requirements. Not all validation errors include fix suggestions, but when - present, they offer specific remediation advice. - type: string - example: - Ensure the name uses only alphanumeric characters, underscores, - and hyphens - type: object - required: - - message - - location - description: - Detailed information about a specific validation error in a request. - Each validation error pinpoints exactly what part of the request failed validation, - why it failed, and how to fix it. Multiple validation errors may be returned - in a single response when there are issues with multiple fields or parameters. - BadRequestErrorDetails: - allOf: - - "$ref": "#/components/schemas/BaseError" - - type: object - properties: - errors: - description: - List of individual validation errors that occurred in the - request. Each error provides specific details about what failed validation, - where the error occurred in the request, and suggestions for fixing - it. This granular information helps developers quickly identify and - resolve multiple issues in a single request without having to make repeated - API calls. - items: - "$ref": "#/components/schemas/ValidationError" - type: array - required: - - errors - description: - Extended error details specifically for bad request (400) errors. - This builds on the BaseError structure by adding an array of individual validation - errors that provide specific information about each validation failure in - the request. This is particularly useful for requests with multiple fields - that might have different validation issues simultaneously. - NotFoundErrorResponse: - type: object - required: - - meta - - error - properties: - meta: - "$ref": "#/components/schemas/Meta" - error: - "$ref": "#/components/schemas/BaseError" - description: - Error response when the requested resource cannot be found. This - typically indicates that the resource either doesn't exist, has been deleted, - or the caller doesn't have permission to see it. Common scenarios include - looking up non-existent keys, APIs, permissions, or identities. When receiving - this error, verify that the resource identifier is correct and that the resource - hasn't been deleted. - ConflictErrorResponse: - type: object - required: - - meta - - error - properties: - meta: - "$ref": "#/components/schemas/Meta" - error: - "$ref": "#/components/schemas/BaseError" - description: - Error response for conflicts with the current state of a resource. - This typically occurs when trying to create a resource that already exists - (like an identity with a duplicate externalId) or when performing an operation - that conflicts with the resource's current state. When receiving this error, - the request should be modified to resolve the conflict before retrying, or - a different operation should be used instead. - UnauthorizedErrorResponse: - type: object - required: - - meta - - error - properties: - meta: - "$ref": "#/components/schemas/Meta" - error: - "$ref": "#/components/schemas/BaseError" - description: |- - Error response when authentication has failed or credentials are missing. This occurs when: - - - The Authorization header is missing - - The root key is invalid or has been revoked - - The root key format is incorrect - - The authentication token has expired - - To fix this error: - 1. Ensure you're including the `Authorization` header with format: `Bearer your_root_key` - 2. Verify your root key is valid and has not been revoked in the Unkey dashboard - 3. Check that you're using the correct root key for the environment - 4. If using a new key, ensure it was created successfully - - For security reasons, the specific reason for authentication failure may be intentionally vague in the error message. Check your Unkey dashboard for more detailed information about your root keys. - ForbiddenErrorResponse: - type: object - required: - - meta - - error - properties: - meta: - "$ref": "#/components/schemas/Meta" - error: - "$ref": "#/components/schemas/BaseError" - description: |- - Error response when the caller is authenticated but lacks permission to perform the requested operation. This occurs when: - - - The root key doesn't have the required permissions for the operation - - The caller is trying to access resources from a different workspace - - The caller is attempting to access another user's resources - - The operation violates a policy restriction - - Unlike Unauthorized (401) which indicates authentication issues, Forbidden (403) indicates authorization problems for an authenticated caller. - - To fix this error: - 1. Check the permissions assigned to your root key in the Unkey dashboard - 2. Verify you're operating within the correct workspace - 3. Ensure you have the necessary scope to access the requested resource - 4. Request additional permissions if needed from your workspace administrator - - Permission patterns in Unkey follow a hierarchical structure: - - 'resource.*' grants all permissions for a resource - - 'resource.read' grants read-only access - - 'resource.write' grants write access - - Common permission requirements for endpoints include: - - keys.create - For creating new API keys - - keys.read - For retrieving key information - - keys.update - For modifying existing keys - - keys.delete - For removing keys - - apis.* - For managing API namespaces - PreconditionFailedErrorResponse: - type: object - required: - - meta - - error - properties: - meta: - "$ref": "#/components/schemas/Meta" - error: - "$ref": "#/components/schemas/BaseError" - description: - Error response for when the service is available but in a degraded - state. This occurs when preconditions for normal operation aren't fully met. - This could happen when dependent services are experiencing issues, when the - system is in maintenance mode, or when certain features are temporarily disabled. - Clients should proceed with caution and may want to retry non-critical operations - later. - BadRequestErrorResponse: - type: object - required: - - meta - - error - properties: - meta: - "$ref": "#/components/schemas/Meta" - error: - "$ref": "#/components/schemas/BadRequestErrorDetails" - description: - Error response for invalid requests that cannot be processed due - to client-side errors. This typically occurs when request parameters are missing, - malformed, or fail validation rules. The response includes detailed information - about the specific errors in the request, including the location of each error - and suggestions for fixing it. When receiving this error, check the 'errors' - array in the response for specific validation issues that need to be addressed - before retrying. - InternalServerErrorResponse: - type: object - required: - - meta - - error - properties: - meta: - "$ref": "#/components/schemas/Meta" - error: - "$ref": "#/components/schemas/BaseError" - description: - Error response for unexpected server-side issues that prevented - the request from being processed correctly. This is typically caused by problems - with the service infrastructure, database connectivity issues, unexpected - exceptions, or service failures. When receiving this error, clients should - implement appropriate retry strategies with backoff and report the issue if - it persists. The `requestId` in the `meta` object is essential for troubleshooting - and should be included in any support inquiries. - Identity: - type: object - properties: - externalId: - type: string - description: External identity ID - meta: - type: object - description: Identity metadata - ratelimits: - type: array - items: - "$ref": "#/components/schemas/RatelimitResponse" - description: Identity ratelimits - required: - - externalId - - ratelimits - KeyCreditsData: - type: object - description: Credit configuration and remaining balance for this key. - properties: - remaining: - type: integer - format: int64 - nullable: true - minimum: 0 - maximum: 9223372036854775807 - description: Number of credits remaining (null for unlimited). - example: 1000 - refill: - "$ref": "#/components/schemas/KeyCreditsRefill" - required: - - remaining - additionalProperties: false - KeyCreditsRefill: - type: object - properties: - interval: - type: string - enum: - - daily - - monthly - description: How often credits are automatically refilled. - example: daily - amount: - type: integer - format: int64 - minimum: 1 - maximum: 1000000 - description: Number of credits added during each refill. - example: 100 - refillDay: - type: integer - minimum: 1 - maximum: 31 - description: Day of month for monthly refills (1-31). - example: 1 - required: - - interval - - amount - additionalProperties: false - KeyResponseData: - type: object - properties: - keyId: - type: string - minLength: 8 - maxLength: 255 - pattern: "^[a-zA-Z0-9_]+$" - description: Unique identifier for this key. - example: key_1234567890abcdef - start: - type: string - minLength: 1 - maxLength: 50 - description: First few characters of the key for identification. - example: sk_test_abc123 - enabled: - type: boolean - description: Whether the key is enabled or disabled. - example: true - name: - type: string - maxLength: 255 - description: Human-readable name for this key. - example: Production API Key - meta: - type: object - additionalProperties: true - maxProperties: 100 - description: Custom metadata associated with this key. - example: - plan: premium - region: us-east-1 - createdAt: - type: integer - format: int64 - minimum: 0 - maximum: 9223372036854775807 - description: Unix timestamp in milliseconds when key was created. - example: 1701425400000 - updatedAt: - type: integer - format: int64 - minimum: 0 - maximum: 9223372036854775807 - description: Unix timestamp in milliseconds when key was last updated. - example: 1701425400000 - expires: - type: integer - format: int64 - minimum: 0 - maximum: 9223372036854775807 - description: Unix timestamp in milliseconds when key expires. - example: 1733000000000 - credits: - "$ref": "#/components/schemas/KeyCreditsData" - plaintext: - type: string - description: Decrypted key value (only when decrypt=true). - example: sk_test_abc123def456 - roles: - type: array - maxItems: 100 - items: - type: string - minLength: 1 - maxLength: 255 - description: Role names assigned to this key. - example: ["admin", "user"] - permissions: - type: array - maxItems: 100 - items: - type: string - minLength: 1 - maxLength: 255 - description: Permission names assigned to this key. - example: ["users.read", "posts.write"] - identity: - "$ref": "#/components/schemas/Identity" - ratelimits: - type: array - maxItems: 50 - items: - "$ref": "#/components/schemas/RatelimitResponse" - description: Rate limit configurations for this key. - required: - - keyId - - start - - apiId - - createdAt - - enabled - additionalProperties: false - LivenessResponseData: - type: object - properties: - message: - description: - Status message indicating the health of the service. A value - of 'OK' indicates that the service is functioning properly and ready to - accept requests. Any other value indicates a potential issue with the - service health. - example: OK - type: string - required: - - message - description: - Response data for the liveness check endpoint. This provides a - simple indication of whether the Unkey API service is running and able to - process requests. Monitoring systems can use this endpoint to track service - availability and trigger alerts if the service becomes unhealthy. - RatelimitResponse: - type: object - properties: - id: - type: string - minLength: 8 - maxLength: 255 - pattern: "^rl_[a-zA-Z0-9_]+$" - description: Unique identifier for this rate limit configuration. - example: rl_1234567890abcdef - name: - type: string - minLength: 1 - maxLength: 128 - pattern: "^[a-zA-Z][a-zA-Z0-9_-]*$" - description: Human-readable name for this rate limit. - example: api_requests - limit: - type: integer - format: int64 - minimum: 1 - maximum: 1000000 - description: Maximum requests allowed within the time window. - example: 1000 - duration: - type: integer - format: int64 - minimum: 1000 - maximum: 2592000000 - description: Rate limit window duration in milliseconds. - example: 3600000 - autoApply: - type: boolean - description: Whether this rate limit was automatically applied when verifying the key. - example: true - required: - - id - - name - - limit - - duration - - autoApply - additionalProperties: false - V2LivenessResponseBody: - type: object - required: - - meta - - data - properties: - meta: - "$ref": "#/components/schemas/Meta" - data: - "$ref": "#/components/schemas/LivenessResponseData" - V2RatelimitSetOverrideRequestBody: - description: |- - Sets a new or overwrites an existing rate limit override. Overrides allow you to apply special rate limit rules to specific identifiers, providing custom limits that differ from the default. - - Overrides are useful for: - - Granting higher limits to premium users or trusted partners - - Implementing stricter limits for suspicious or abusive users - - Creating tiered access levels with different quotas - - Implementing temporary rate limit adjustments - - Prioritizing important clients with higher limits - additionalProperties: false - properties: - namespace: - type: string - minLength: 1 - maxLength: 255 - description: | - The rate limit namespace identifier. This can be either: - - A namespace name (e.g., "api.requests", "auth.login") - human-readable names that are unique within your workspace - - A namespace ID (e.g., "ns_1234567890abcdef") - system-generated unique identifiers - - The system will automatically detect whether you've provided a name or ID and perform the appropriate lookup. - Names must start with a letter and can contain letters, numbers, underscores, dots, slashes, or hyphens. - IDs follow the format "ns_" followed by an alphanumeric string. - example: api.requests - duration: - description: |- - The duration in milliseconds for the rate limit window. This defines how long the rate limit counter accumulates before resetting to zero. - - Considerations: - - This can differ from the default duration for the namespace - - Longer durations create stricter limits that take longer to reset - - Shorter durations allow more frequent bursts of activity - - Common values: 60000 (1 minute), 3600000 (1 hour), 86400000 (1 day) - format: int64 - type: integer - minimum: 1000 - identifier: - description: |- - Identifier of the entity receiving this custom rate limit. This can be: - - - A specific user ID for individual custom limits - - An IP address for location-based rules - - An email domain for organization-wide policies - - Any other string that identifies the target entity - - Wildcards (*) can be used to create pattern-matching rules that apply to multiple identifiers. For example: - - 'premium_*' would match all identifiers starting with 'premium_' - - '*_admin' would match all identifiers ending with '_admin' - - '*suspicious*' would match any identifier containing 'suspicious' - - More detailed information on wildcard pattern rules is available at https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules - type: string - minLength: 1 - maxLength: 255 - limit: - description: |- - The maximum number of requests allowed for this override. This defines the custom quota for the specified identifier(s). - - Special values: - - Higher than default: For premium or trusted entities - - Lower than default: For suspicious or abusive entities - - 0: To completely block access (useful for ban implementation) - - This limit entirely replaces the default limit for matching identifiers. - format: int64 - type: integer - minimum: 0 - required: - - namespace - - identifier - - limit - - duration - type: object - RatelimitSetOverrideResponseData: - type: object - properties: - overrideId: - description: |- - The unique identifier for the newly created or updated rate limit override. This ID can be used to: - - - Reference this specific override in subsequent API calls - - Delete or modify this override later - - Track which override is being applied in rate limit responses - - Associate override effects with specific rules in analytics - - Store this ID if you need to manage the override in the future. - type: string - required: - - overrideId - V2RatelimitSetOverrideResponseBody: - type: object - required: - - meta - - data - properties: - meta: - "$ref": "#/components/schemas/Meta" - data: - "$ref": "#/components/schemas/RatelimitSetOverrideResponseData" - V2RatelimitGetOverrideRequestBody: - description: |- - Gets the configuration of an existing rate limit override. Use this to retrieve details about custom rate limit rules that have been created for specific identifiers within a namespace. - - This endpoint is useful for: - - Verifying override configurations - - Checking current limits for specific entities - - Auditing rate limit policies - - Debugging rate limiting behavior - - Retrieving override settings for modification - additionalProperties: false - properties: - namespace: - type: string - minLength: 1 - maxLength: 255 - description: | - The rate limit namespace identifier. This can be either: - - A namespace name (e.g., "api.requests", "auth.login") - human-readable names that are unique within your workspace - - A namespace ID (e.g., "ns_1234567890abcdef") - system-generated unique identifiers - - The system will automatically detect whether you've provided a name or ID and perform the appropriate lookup. - Names must start with a letter and can contain letters, numbers, underscores, dots, slashes, or hyphens. - IDs follow the format "ns_" followed by an alphanumeric string. - example: api.requests - identifier: - description: |- - The exact identifier pattern for the override you want to retrieve. This must match exactly as it was specified when creating the override. - - Important notes: - - This is case-sensitive and must match exactly - - Include any wildcards (*) that were part of the original pattern - - For example, if the override was created for 'premium_*', you must use 'premium_*' here, not a specific ID like 'premium_user1' - - This field is used to look up the specific override configuration for this pattern. - type: string - minLength: 1 - maxLength: 255 - required: - - namespace - - identifier - type: object - V2RatelimitGetOverrideResponseBody: - type: object - required: - - meta - - data - properties: - meta: - "$ref": "#/components/schemas/Meta" - data: - "$ref": "#/components/schemas/RatelimitOverride" - V2RatelimitListOverridesRequestBody: - additionalProperties: false - properties: - namespace: - type: string - minLength: 1 - maxLength: 255 - description: | - The rate limit namespace identifier. This can be either: - - A namespace name (e.g., "api.requests", "auth.login") - human-readable names that are unique within your workspace - - A namespace ID (e.g., "ns_1234567890abcdef") - system-generated unique identifiers - - The system will automatically detect whether you've provided a name or ID and perform the appropriate lookup. - Names must start with a letter and can contain letters, numbers, underscores, dots, slashes, or hyphens. - IDs follow the format "ns_" followed by an alphanumeric string. - example: api.requests - cursor: - description: - Pagination cursor from a previous response. Include this when - fetching subsequent pages of results. Each response containing more results - than the requested limit will include a cursor value in the pagination - object that can be used here. - type: string - limit: - description: |- - Maximum number of override entries to return in a single response. Use this to control response size and loading performance. - - - Lower values (10-20): Better for UI displays and faster response times - - Higher values (50-100): Better for data exports or bulk operations - - Default (10): Suitable for most dashboard views - - Results exceeding this limit will be paginated, with a cursor provided for fetching subsequent pages. - type: integer - default: 10 - minimum: 1 - maximum: 100 - required: - - namespace - type: object - RatelimitListOverridesResponseData: - type: array - items: - "$ref": "#/components/schemas/RatelimitOverride" - V2RatelimitListOverridesResponseBody: - type: object - required: - - meta - - data - properties: - meta: - "$ref": "#/components/schemas/Meta" - data: - "$ref": "#/components/schemas/RatelimitListOverridesResponseData" - pagination: - "$ref": "#/components/schemas/Pagination" - V2RatelimitLimitRequestBody: - additionalProperties: false - properties: - namespace: - type: string - minLength: 1 - maxLength: 255 - description: | - The rate limit namespace identifier. This can be either: - - A namespace name (e.g., "api.requests", "auth.login") - human-readable names that are unique within your workspace - - A namespace ID (e.g., "ns_1234567890abcdef") - system-generated unique identifiers - - The system will automatically detect whether you've provided a name or ID and perform the appropriate lookup. - Names must start with a letter and can contain letters, numbers, underscores, dots, slashes, or hyphens. - IDs follow the format "ns_" followed by an alphanumeric string. - example: api.requests - cost: - type: integer - format: int64 - minimum: 0 - maximum: 1000 # Reasonable upper bound for operation costs - default: 1 - description: | - Sets how much of the rate limit quota this request consumes, enabling weighted rate limiting. - Use higher values for resource-intensive operations and 0 for tracking without limiting. - When accumulated cost exceeds the limit within the duration window, subsequent requests are rejected. - Essential for implementing fair usage policies and preventing resource abuse through expensive operations. - example: 5 - duration: - type: integer - format: int64 - minimum: 1000 # 1 second minimum window - maximum: 2592000000 # 30 days maximum window - description: | - Sets the rate limit window duration in milliseconds after which the counter resets. - Shorter durations enable faster recovery but may be less effective against sustained abuse. - Common values include 60000 (1 minute), 3600000 (1 hour), and 86400000 (24 hours). - Balance user experience with protection needs when choosing window sizes. - example: 60000 - identifier: - type: string - minLength: 1 - maxLength: 255 # Reasonable upper bound for identifiers - pattern: "^[a-zA-Z0-9_.:/-]+$" - description: | - Defines the scope of rate limiting by identifying the entity being limited. - Use user IDs for per-user limits, IP addresses for anonymous limiting, or API key IDs for per-key limits. - Accepts letters, numbers, underscores, dots, colons, slashes, and hyphens for flexible identifier formats. - The same identifier can be used across different namespaces to apply multiple rate limit types. - Choose identifiers that provide appropriate granularity for your rate limiting strategy. - example: "user_12345" - limit: - type: integer - format: int64 - minimum: 1 - maximum: 1000000 # Reasonable upper bound for rate limits - description: | - Sets the maximum operations allowed within the duration window before requests are rejected. - When this limit is reached, subsequent requests fail with `RATE_LIMITED` until the window resets. - Balance user experience with resource protection when setting limits for different user tiers. - Consider system capacity, business requirements, and fair usage policies in limit determination. - example: 1000 - required: - - namespace - - identifier - - limit - - duration - type: object - RatelimitLimitResponseData: - type: object - properties: - limit: - description: |- - The maximum number of operations allowed within the time window. This reflects either the default limit specified in the request or an override limit if one exists for this identifier. - - This value helps clients understand their total quota for the current window. - format: int64 - type: integer - remaining: - description: |- - The number of operations remaining in the current window before the rate limit is exceeded. Applications should use this value to: - - - Implement client-side throttling before hitting limits - - Display usage information to end users - - Trigger alerts when approaching limits - - Adjust request patterns based on available capacity - - When this reaches zero, requests will be rejected until the window resets. - format: int64 - type: integer - reset: - description: |- - The Unix timestamp in milliseconds when the rate limit window will reset and 'remaining' will return to 'limit'. - - This timestamp enables clients to: - - Calculate and display wait times to users - - Implement intelligent retry mechanisms - - Schedule requests to resume after the reset - - Implement exponential backoff when needed - - The reset time is based on a sliding window from the first request in the current window. - format: int64 - type: integer - success: - description: |- - Whether the request passed the rate limit check. If true, the request is allowed to proceed. If false, the request has exceeded the rate limit and should be blocked or rejected. - - You MUST check this field to determine if the request should proceed, as the endpoint always returns `HTTP 200` even when rate limited. - type: boolean - overrideId: - description: |- - If a rate limit override was applied for this identifier, this field contains the ID of the override that was used. Empty when no override is in effect. - - This can be useful for: - - Debugging which override rule was matched - - Tracking the effects of specific overrides - - Understanding why limits differ from default values - - Audit logging of special rate limit rules - type: string - required: - - limit - - remaining - - reset - - success - V2RatelimitLimitResponseBody: - type: object - required: - - meta - - data - properties: - meta: - "$ref": "#/components/schemas/Meta" - data: - "$ref": "#/components/schemas/RatelimitLimitResponseData" - V2RatelimitDeleteOverrideRequestBody: - description: |- - Deletes an existing rate limit override. This permanently removes a custom rate limit rule, reverting affected identifiers back to the default rate limits for the namespace. - - Use this endpoint when you need to: - - Remove special rate limit rules that are no longer needed - - Reset entities back to standard rate limits - - Clean up temporary overrides - - Remove outdated tiering or custom limit rules - - Fix misconfigured overrides - - Once deleted, the override cannot be recovered, and the operation takes effect immediately. - additionalProperties: false - properties: - namespace: - type: string - minLength: 1 - maxLength: 255 - description: | - The rate limit namespace identifier. This can be either: - - A namespace name (e.g., "api.requests", "auth.login") - human-readable names that are unique within your workspace - - A namespace ID (e.g., "ns_1234567890abcdef") - system-generated unique identifiers - - The system will automatically detect whether you've provided a name or ID and perform the appropriate lookup. - Names must start with a letter and can contain letters, numbers, underscores, dots, slashes, or hyphens. - IDs follow the format "ns_" followed by an alphanumeric string. - example: api.requests - identifier: - description: |- - The exact identifier pattern of the override to delete. This must match exactly as it was specified when creating the override. - - Important notes: - - This is case-sensitive and must match exactly - - Include any wildcards (*) that were part of the original pattern - - For example, if the override was created for 'premium_*', you must use 'premium_*' here, not a specific ID - - After deletion, any identifiers previously affected by this override will immediately revert to using the default rate limit for the namespace. - type: string - minLength: 1 - maxLength: 255 - required: - - namespace - - identifier - type: object - RatelimitDeleteOverrideResponseData: - type: object - additionalProperties: false - description: - Empty response object. A successful response indicates the override - was successfully deleted. The operation is immediate - as soon as this response - is received, the override no longer exists and affected identifiers have reverted - to using the default rate limit for the namespace. No other data is returned - as part of the deletion operation. - V2RatelimitDeleteOverrideResponseBody: - type: object - required: - - meta - - data - properties: - meta: - "$ref": "#/components/schemas/Meta" - data: - "$ref": "#/components/schemas/RatelimitDeleteOverrideResponseData" - V2IdentitiesCreateIdentityRequestBody: - type: object - required: - - externalId - properties: - externalId: - type: string - minLength: 3 - maxLength: 255 # Match database varchar limits for external identifiers - pattern: "^[a-zA-Z0-9_.-]+$" - description: | - Creates an identity using your system's unique identifier for a user, organization, or entity. - Must be stable and unique across your workspace - duplicate externalIds return CONFLICT errors. - This identifier links Unkey identities to your authentication system, database records, or tenant structure. - - Avoid changing externalIds after creation as this breaks the link between your systems. - Use consistent identifier patterns across your application for easier management and debugging. - Accepts letters, numbers, underscores, dots, and hyphens for flexible identifier formats. - Essential for implementing proper multi-tenant isolation and user-specific rate limiting. - example: user_123 - meta: - type: object - additionalProperties: true - maxProperties: 100 # Prevent DoS while allowing rich metadata - description: | - Stores arbitrary JSON metadata returned during key verification for contextual information. - Eliminates additional database lookups during verification, improving performance for stateless services. - Avoid storing sensitive data here as it's returned in verification responses. - - Large metadata objects increase verification latency and should stay under 10KB total size. - Use this for subscription details, feature flags, user preferences, and organization information. - Metadata is returned as-is whenever keys associated with this identity are verified. - ratelimits: - type: array - maxItems: 50 # Reasonable limit for rate limit configurations per identity - items: - "$ref": "#/components/schemas/RatelimitRequest" - description: | - Defines shared rate limits that apply to all keys belonging to this identity. - Prevents abuse by users with multiple keys by enforcing consistent limits across their entire key portfolio. - Essential for implementing fair usage policies and tiered access levels in multi-tenant applications. - - Rate limit counters are shared across all keys with this identity, regardless of how many keys the user creates. - During verification, specify which named limits to check for enforcement. - Identity rate limits supplement any key-specific rate limits that may also be configured. - - Each named limit can have different thresholds and windows - - When verifying keys, you can specify which limits you want to use and all keys attached to this identity will share the limits, regardless of which specific key is used. - RatelimitRequest: - type: object - required: - - name - - limit - - duration - - autoApply - properties: - name: - description: |- - The name of this rate limit. This name is used to identify which limit to check during key verification. - - Best practices for limit names: - - Use descriptive, semantic names like 'api_requests', 'heavy_operations', or 'downloads' - - Be consistent with naming conventions across your application - - Create separate limits for different resource types or operation costs - - Consider using namespaced names for better organization (e.g., 'files.downloads', 'compute.training') - - You will reference this exact name when verifying keys to check against this specific limit. - type: string - example: api - minLength: 3 - maxLength: 128 - limit: - description: |- - The maximum number of operations allowed within the specified time window. - - When this limit is reached, verification requests will fail with `code=RATE_LIMITED` until the window resets. The limit should reflect: - - Your infrastructure capacity and scaling limitations - - Fair usage expectations for your service - - Different tier levels for various user types - - The relative cost of the operations being limited - - Higher values allow more frequent access but may impact service performance. - type: integer - format: int64 - minimum: 1 - duration: - description: |- - The duration for each ratelimit window in milliseconds. - - This controls how long the rate limit counter accumulates before resetting. Common values include: - - 1000 (1 second): For strict per-second limits on high-frequency operations - - 60000 (1 minute): For moderate API usage control - - 3600000 (1 hour): For less frequent but costly operations - - 86400000 (24 hours): For daily quotas - - Shorter windows provide more frequent resets but may allow large burst usage. Longer windows provide more consistent usage patterns but take longer to reset after limit exhaustion. - type: integer - format: int64 - minimum: 1000 - autoApply: - description: |- - Whether this ratelimit should be automatically applied when verifying a key. - type: boolean - default: false - IdentitiesCreateIdentityResponseData: - type: object - V2IdentitiesCreateIdentityResponseBody: - type: object - required: - - meta - - data - properties: - meta: - "$ref": "#/components/schemas/Meta" - data: - "$ref": "#/components/schemas/IdentitiesCreateIdentityResponseData" - V2IdentitiesGetIdentityRequestBody: - type: object - properties: - externalId: - type: string - minLength: 1 - description: - The external ID of the identity to retrieve. This is the ID - from your own system that was used during identity creation. - example: user_abc123 - additionalProperties: false - required: - - externalId - IdentitiesGetIdentityResponseData: - type: object - required: - - externalId - properties: - externalId: - type: string - description: - The external identifier for this identity in your system. This - is the ID you provided during identity creation. - example: user_abc123 - meta: - type: object - additionalProperties: true - description: - Custom metadata associated with this identity. This can include - any JSON-serializable data you stored with the identity during creation - or updates. - example: - name: Alice Smith - email: alice@example.com - plan: premium - ratelimits: - type: array - items: - "$ref": "#/components/schemas/RatelimitResponse" - description: - Rate limits associated with this identity. These limits are - shared across all API keys linked to this identity, providing consistent - rate limiting regardless of which key is used. - IdentitiesListIdentitiesResponseData: - type: array - items: - "$ref": "#/components/schemas/Identity" - description: List of identities matching the specified criteria. - V2IdentitiesGetIdentityResponseBody: - type: object - required: - - meta - - data - properties: - meta: - "$ref": "#/components/schemas/Meta" - data: - "$ref": "#/components/schemas/IdentitiesGetIdentityResponseData" - V2IdentitiesListIdentitiesRequestBody: - type: object - properties: - limit: - type: integer - minimum: 1 - maximum: 100 - default: 100 - description: - The maximum number of identities to return in a single request. - Use this to control response size and loading performance. - example: 50 - cursor: - type: string - description: - Pagination cursor from a previous response. Use this to fetch - subsequent pages of results when the response contains a cursor value. - example: cursor_eyJrZXkiOiJrZXlfMTIzNCJ9 - additionalProperties: false - V2IdentitiesListIdentitiesResponseBody: - type: object - required: - - meta - - data - - pagination - properties: - meta: - "$ref": "#/components/schemas/Meta" - data: - "$ref": "#/components/schemas/IdentitiesListIdentitiesResponseData" - pagination: - "$ref": "#/components/schemas/Pagination" - V2IdentitiesDeleteIdentityRequestBody: - additionalProperties: false - type: object - properties: - externalId: - type: string - minLength: 3 - description: | - The id of this identity in your system. - This should match the externalId value you used when creating the identity. - This identifier typically comes from your authentication system and could be a userId, organizationId, or any other stable unique identifier in your application. - example: user_123 - required: - - externalId - V2IdentitiesDeleteIdentityResponseBody: - type: object - description: - Empty response object. A successful response indicates the identity - was deleted successfully. The operation is immediate and permanent - the identity - and all its associated data are removed from the system. Any API keys previously - associated with this identity remain valid but are no longer linked to this - identity. - properties: - meta: - "$ref": "#/components/schemas/Meta" - required: - - meta - Permission: - type: object - properties: - id: - type: string - minLength: 3 - maxLength: 255 - pattern: "^[a-zA-Z0-9_]+$" - description: | - The unique identifier for this permission within Unkey's system. - Generated automatically when the permission is created and used to reference this permission in API operations. - Always begins with 'perm_' followed by alphanumeric characters and underscores. - example: perm_1234567890abcdef - name: - type: string - minLength: 1 - maxLength: 512 - description: | - The human-readable name for this permission that describes its purpose. - Should be descriptive enough for developers to understand what access it grants. - Use clear, semantic names that reflect the resources or actions being permitted. - Names must be unique within your workspace to avoid confusion and conflicts. - example: "users.read" - slug: - type: string - minLength: 1 - maxLength: 512 - description: | - The URL-safe identifier when this permission was created. - example: users-read - description: - type: string - maxLength: 2048 - description: | - Optional detailed explanation of what this permission grants access to. - Helps team members understand the scope and implications of granting this permission. - Include information about what resources can be accessed and what actions can be performed. - Not visible to end users - this is for internal documentation and team clarity. - example: "Allows reading user profile information and account details" - createdAt: - type: integer - format: int64 - minimum: 0 - maximum: 9223372036854775807 # Max int64 value for future-proofing - description: | - Unix timestamp in milliseconds indicating when this permission was first created. - Useful for auditing and understanding the evolution of your permission structure. - Automatically set by the system and cannot be modified. - example: 1701425400000 - required: - - id - - name - - slug - - createdAt - additionalProperties: false - V2PermissionsCreatePermissionRequestBody: - type: object - required: - - name - - slug - properties: - name: - type: string - minLength: 1 - maxLength: 512 - description: | - Creates a permission with this human-readable name that describes its purpose. - Names must be unique within your workspace to prevent conflicts during assignment. - Use clear, semantic names that developers can easily understand when building authorization logic. - Consider using hierarchical naming conventions like 'resource.action' for better organization. - - Examples: 'users.read', 'billing.write', 'analytics.view', 'admin.manage' - example: "users.read" - slug: - type: string - minLength: 1 - maxLength: 128 - pattern: "^[a-zA-Z][a-zA-Z0-9._-]*$" - description: | - Creates a URL-safe identifier for this permission that can be used in APIs and integrations. - Must start with a letter and contain only letters, numbers, periods, underscores, and hyphens. - Slugs are often used in REST endpoints, configuration files, and external integrations. - Should closely match the name but in a format suitable for technical usage. - Must be unique within your workspace to ensure reliable permission lookups. - - Keep slugs concise but descriptive for better developer experience. - example: "users-read" - description: - type: string - maxLength: 128 - description: | - Provides detailed documentation of what this permission grants access to. - Include information about affected resources, allowed actions, and any important limitations. - This internal documentation helps team members understand permission scope and security implications. - Not visible to end users - designed for development teams and security audits. - - Consider documenting: - - What resources can be accessed - - What operations are permitted - - Any conditions or limitations - - Related permissions that might be needed - example: "Grants read-only access to user profile information, account settings, and subscription status. Does not include access to payment methods or billing history." - additionalProperties: false - PermissionsCreatePermissionResponseData: - type: object - properties: - permissionId: - type: string - minLength: 3 - maxLength: 255 - pattern: "^[a-zA-Z0-9_]+$" - description: | - The unique identifier assigned to the newly created permission. - Use this ID to reference the permission in role assignments, key operations, and other API calls. - Always begins with 'perm_' followed by a unique alphanumeric sequence. - Store this ID if you need to manage or reference this permission in future operations. - example: perm_1234567890abcdef - required: - - permissionId - additionalProperties: false - V2PermissionsCreatePermissionResponseBody: - type: object - required: - - meta - - data - properties: - meta: - "$ref": "#/components/schemas/Meta" - data: - "$ref": "#/components/schemas/PermissionsCreatePermissionResponseData" - V2PermissionsGetPermissionRequestBody: - type: object - required: - - permissionId - properties: - permissionId: - type: string - minLength: 3 - maxLength: 255 - pattern: "^[a-zA-Z0-9_]+$" - description: | - Specifies which permission to retrieve by its unique identifier. - Must be a valid permission ID that begins with 'perm_' and exists within your workspace. - Use this endpoint to verify permission details, check its current configuration, or retrieve metadata. - Returns detailed information including name, description, and workspace association. - example: perm_1234567890abcdef - additionalProperties: false - PermissionsGetPermissionResponseData: - type: object - properties: - permission: - "$ref": "#/components/schemas/Permission" - required: - - permission - additionalProperties: false - description: Complete permission details including ID, name, and metadata. - V2PermissionsGetPermissionResponseBody: - type: object - required: - - meta - - data - properties: - meta: - "$ref": "#/components/schemas/Meta" - data: - "$ref": "#/components/schemas/PermissionsGetPermissionResponseData" - V2PermissionsListPermissionsRequestBody: - type: object - properties: - cursor: - type: string - maxLength: 1024 - description: | - Pagination cursor from a previous response to fetch the next page of permissions. - Include this value when you need to retrieve additional permissions beyond the initial response. - Each response containing more results than the requested limit includes a cursor for subsequent pages. - - Leave empty or omit this field to start from the beginning of the permission list. - Cursors are temporary and may expire - always handle cases where a cursor becomes invalid. - example: "eyJrZXkiOiJwZXJtXzEyMzQifQ==" - limit: - type: integer - minimum: 1 - maximum: 100 - default: 100 - description: Maximum number of permissions to return in a single response. - example: 50 - additionalProperties: false - PermissionsListPermissionsResponseData: - type: array - maxItems: 1000 # DoS protection while allowing reasonable batch sizes - items: - "$ref": "#/components/schemas/Permission" - description: Array of permission objects with complete configuration details. - V2PermissionsListPermissionsResponseBody: - type: object - required: - - meta - - data - properties: - meta: - "$ref": "#/components/schemas/Meta" - data: - "$ref": "#/components/schemas/PermissionsListPermissionsResponseData" - pagination: - "$ref": "#/components/schemas/Pagination" - additionalProperties: false - V2PermissionsListRolesRequestBody: - type: object - properties: - limit: - type: integer - minimum: 1 - maximum: 100 - default: 100 - description: | - Maximum number of roles to return in a single response. - Use smaller values for faster response times and better UI performance. - Use larger values when you need to process many roles efficiently. - Results exceeding this limit will be paginated with a cursor for continuation. - example: 50 - cursor: - type: string - maxLength: 1024 - description: | - Pagination cursor from a previous response to fetch the next page of roles. - Include this when you need to retrieve additional roles beyond the first page. - Each response containing more results will include a cursor value that can be used here. - Leave empty or omit this field to start from the beginning of the role list. - example: "eyJrZXkiOiJyb2xlXzEyMzQifQ==" - additionalProperties: false - PermissionsListRolesResponseData: - type: array - maxItems: 1000 - items: - "$ref": "#/components/schemas/RoleWithPermissions" - description: Array of roles with their assigned permissions. - - V2PermissionsDeleteRoleRequestBody: - type: object - required: - - roleId - properties: - roleId: - type: string - minLength: 8 - maxLength: 255 - pattern: "^[a-zA-Z0-9_]+$" - description: | - Unique identifier of the role to permanently delete from your workspace. - Must be a valid role ID that begins with 'role_' and exists within your workspace. - - WARNING: Deletion is immediate and irreversible with significant consequences: - - All API keys assigned this role will lose the associated permissions - - Access to resources protected by this role's permissions will be denied - - Any authorization logic depending on this role will start failing - - Historical analytics and audit logs referencing this role remain intact - - Before deletion, ensure: - - You have the correct role ID (verify the role name and permissions) - - You've updated any dependent authorization logic or code - - You've migrated any keys to use alternative roles or direct permissions - - You've notified relevant team members of the access changes - example: role_dns_manager - additionalProperties: false - V2PermissionsDeleteRoleResponseBody: - type: object - required: - - meta - properties: - meta: - "$ref": "#/components/schemas/Meta" - additionalProperties: false - V2PermissionsListRolesResponseBody: - type: object - required: - - meta - - data - properties: - meta: - "$ref": "#/components/schemas/Meta" - data: - "$ref": "#/components/schemas/PermissionsListRolesResponseData" - pagination: - "$ref": "#/components/schemas/Pagination" - additionalProperties: false - V2PermissionsDeletePermissionRequestBody: - type: object - required: - - permissionId - properties: - permissionId: - type: string - minLength: 3 - maxLength: 255 - pattern: "^[a-zA-Z0-9_]+$" - description: | - Specifies which permission to permanently delete from your workspace. - - WARNING: Deleting a permission has immediate and irreversible consequences: - - All API keys with this permission will lose that access immediately - - All roles containing this permission will have it removed - - Any verification requests checking for this permission will fail - - This action cannot be undone - - Before deletion, ensure you: - - Have updated any keys or roles that depend on this permission - - Have migrated to alternative permissions if needed - - Have notified affected users about the access changes - - Have the correct permission ID (double-check against your permission list) - example: perm_1234567890abcdef - additionalProperties: false - V2PermissionsDeletePermissionResponseBody: - type: object - required: - - meta - properties: - meta: - "$ref": "#/components/schemas/Meta" - additionalProperties: false - V2PermissionsCreateRoleRequestBody: - type: object - required: - - name - properties: - name: - type: string - minLength: 1 - maxLength: 512 - pattern: "^[a-zA-Z][a-zA-Z0-9._-]*$" - description: | - Creates a role with this unique name that groups related permissions together. - Names must be unique within your workspace to prevent conflicts during assignment. - Use descriptive names that clearly indicate the role's purpose and scope of access. - Consider hierarchical naming conventions like 'department.function' for better organization. - - Role names should be: - - Descriptive enough to understand their purpose - - Consistent with your organization's naming conventions - - Unique to avoid confusion during role assignment - - Focused on a specific function or responsibility - - Examples: 'admin.billing', 'support.readonly', 'developer.api', 'manager.analytics' - example: "support.readonly" - description: - type: string - maxLength: 2048 - description: | - Provides comprehensive documentation of what this role encompasses and what access it grants. - Include information about the intended use case, what permissions should be assigned, and any important considerations. - This internal documentation helps team members understand role boundaries and security implications. - Not visible to end users - designed for administration teams and access control audits. - - Consider documenting: - - The role's intended purpose and scope - - What types of users should receive this role - - What permissions are typically associated with it - - Any security considerations or limitations - - Related roles that might be used together - example: "Provides read-only access for customer support representatives. Includes permissions to view user accounts, support tickets, and basic analytics. Does not include access to billing, admin functions, or data modification capabilities." - additionalProperties: false - PermissionsCreateRoleResponseData: - type: object - properties: - roleId: - type: string - minLength: 3 - maxLength: 255 - pattern: "^[a-zA-Z0-9_]+$" - description: | - The unique identifier assigned to the newly created role. - Use this ID to reference the role in permission assignments, key operations, and role management calls. - Always begins with 'role_' followed by a unique alphanumeric sequence. - Store this ID if you need to manage, modify, or assign this role in future operations. - example: role_1234567890abcdef - required: - - roleId - additionalProperties: false - V2PermissionsCreateRoleResponseBody: - type: object - required: - - meta - - data - properties: - meta: - "$ref": "#/components/schemas/Meta" - data: - "$ref": "#/components/schemas/PermissionsCreateRoleResponseData" - Role: - type: object - properties: - id: - type: string - minLength: 3 - maxLength: 255 - pattern: "^[a-zA-Z0-9_]+$" - description: | - The unique identifier for this role within Unkey's system. - Generated automatically when the role is created and used to reference this role in API operations. - Always begins with 'role_' followed by alphanumeric characters and underscores. - example: role_1234567890abcdef - name: - type: string - minLength: 1 - maxLength: 512 - description: | - The human-readable name for this role that describes its function. - Should be descriptive enough for administrators to understand what access this role provides. - Use clear, semantic names that reflect the job function or responsibility level. - Names must be unique within your workspace to avoid confusion during role assignment. - example: "support.readonly" - description: - type: string - maxLength: 2048 - description: | - Optional detailed explanation of what this role encompasses and what access it provides. - Helps team members understand the role's scope, intended use cases, and security implications. - Include information about what types of users should receive this role and what they can accomplish. - Not visible to end users - this is for internal documentation and access control audits. - example: "Provides read-only access for customer support representatives to view user accounts and support tickets" - createdAt: - type: integer - format: int64 - minimum: 0 - maximum: 9223372036854775807 # Max int64 value for future-proofing - description: | - Unix timestamp in milliseconds indicating when this role was first created. - Useful for auditing and understanding the evolution of your access control structure. - Automatically set by the system and cannot be modified. - example: 1701425400000 - required: - - id - - name - - createdAt - additionalProperties: false - RoleWithPermissions: - type: object - properties: - id: - type: string - minLength: 3 - maxLength: 255 - pattern: "^[a-zA-Z0-9_]+$" - description: | - The unique identifier for this role within Unkey's system. - Generated automatically when the role is created and used to reference this role in API operations. - Always begins with 'role_' followed by alphanumeric characters and underscores. - example: role_1234567890abcdef - name: - type: string - minLength: 1 - maxLength: 512 - description: | - The human-readable name for this role that describes its function. - Should be descriptive enough for administrators to understand what access this role provides. - Use clear, semantic names that reflect the job function or responsibility level. - Names must be unique within your workspace to avoid confusion during role assignment. - example: "support.readonly" - description: - type: string - maxLength: 2048 - description: | - Optional detailed explanation of what this role encompasses and what access it provides. - Helps team members understand the role's scope, intended use cases, and security implications. - Include information about what types of users should receive this role and what they can accomplish. - Not visible to end users - this is for internal documentation and access control audits. - example: "Provides read-only access for customer support representatives to view user accounts and support tickets" - createdAt: - type: integer - format: int64 - minimum: 0 - maximum: 9223372036854775807 # Max int64 value for future-proofing - description: | - Unix timestamp in milliseconds indicating when this role was first created. - Useful for auditing and understanding the evolution of your access control structure. - Automatically set by the system and cannot be modified. - example: 1701425400000 - permissions: - type: array - items: - "$ref": "#/components/schemas/Permission" - maxItems: 100 - description: | - Complete list of permissions currently assigned to this role. - Each permission grants specific access rights that will be inherited by any keys or users assigned this role. - Use this list to understand the full scope of access provided by this role. - Permissions can be added or removed from roles without affecting the role's identity or other properties. - Empty array indicates a role with no permissions currently assigned. - required: - - id - - name - - permissions - - createdAt - additionalProperties: false - V2PermissionsGetRoleRequestBody: - type: object - required: - - roleId - properties: - roleId: - type: string - minLength: 3 - maxLength: 255 - pattern: "^[a-zA-Z0-9_]+$" - description: | - Specifies which role to retrieve by its unique identifier. - Must be a valid role ID that begins with 'role_' and exists within your workspace. - Use this endpoint to verify role details, check its current permissions, or retrieve metadata. - Returns complete role information including all assigned permissions for comprehensive access review. - example: role_1234567890abcdef - additionalProperties: false - PermissionsGetRoleResponseData: - type: object - properties: - role: - "$ref": "#/components/schemas/RoleWithPermissions" - required: - - role - additionalProperties: false - description: Complete role details including assigned permissions. - V2PermissionsGetRoleResponseBody: - type: object - required: - - meta - - data - properties: - meta: - "$ref": "#/components/schemas/Meta" - data: - "$ref": "#/components/schemas/PermissionsGetRoleResponseData" - additionalProperties: false - V2ApisCreateApiRequestBody: - type: object - required: - - name - properties: - name: - type: string - minLength: 3 - maxLength: 255 # Reasonable upper bound for API names - pattern: "^[a-zA-Z][a-zA-Z0-9._-]*$" # Must start with letter, then alphanumeric/underscore/dot/hyphen - description: | - Creates an API with this name as an internal identifier for organization and isolation. - APIs serve as containers for groups of keys and provide namespace separation between environments or services. - Names must be unique within your workspace and are not shown to end users. - - Use descriptive names that clearly identify the API's purpose and environment: - - Include environment indicators like 'production', 'staging', 'development' - - Use service-based naming like 'payment-service', 'user-management', 'analytics' - - Follow consistent naming conventions across your organization - - Keep names concise but informative for easy identification - - Must start with a letter and contain only letters, numbers, underscores, dots, and hyphens. - Avoid generic names like 'api' or 'main' that don't provide meaningful context. - example: payment-service-production - additionalProperties: false - ApisCreateApiResponseData: - type: object - properties: - apiId: - type: string - minLength: 8 - maxLength: 255 - pattern: "^[a-zA-Z0-9_]+$" - description: | - The unique identifier assigned to the newly created API. - Use this ID for all subsequent operations including key creation, verification, and API management. - Always begins with 'api_' followed by a unique alphanumeric sequence. - - Store this ID securely as it's required when: - - Creating API keys within this namespace - - Verifying keys associated with this API - - Managing API settings and metadata - - Listing keys belonging to this API - - This identifier is permanent and cannot be changed after creation. - example: api_2cGKbMxRjIzhCxo1IdjH3a - required: - - apiId - additionalProperties: false - V2ApisDeleteApiRequestBody: - type: object - required: - - apiId - properties: - apiId: - type: string - minLength: 8 - maxLength: 255 - pattern: "^[a-zA-Z0-9_]+$" - description: | - Specifies which API to permanently delete from your workspace. - Must be a valid API ID that begins with 'api_' and exists within your workspace. - - CRITICAL WARNING: Deletion is immediate and irreversible with severe consequences: - - ALL keys associated with this API become invalid instantly - - Verification requests for these keys will fail with `code=NOT_FOUND` - - Client applications using these keys will lose access immediately - - Analytics data and key metadata are permanently removed - - This operation cannot be undone under any circumstances - - Before proceeding, ensure you have: - - Verified the correct API ID (double-check environment and service) - - Migrated all active keys to alternative APIs - - Updated all client applications to use replacement keys - - Backed up critical analytics data and key configurations - - Notified all stakeholders of the service interruption - - Tested replacement systems in non-production environments - - Consider disabling keys first to test impact before permanent deletion. - example: api_VNcuGfVjUkrVcWJmda0A - additionalProperties: false - V2ApisGetApiRequestBody: - type: object - required: - - apiId - properties: - apiId: - type: string - minLength: 8 - maxLength: 255 - pattern: "^[a-zA-Z0-9_]+$" - description: | - Specifies which API to retrieve by its unique identifier. - Must be a valid API ID that begins with 'api_' and exists within your workspace. - - Use this endpoint to: - - Verify an API exists and is accessible - - Retrieve the API's current name and configuration - - Validate API IDs before performing key operations - - Check API status during debugging or troubleshooting - - Returns complete API information including name, ID, and metadata. - example: api_1234567890abcdef - additionalProperties: false - ApisGetApiResponseData: - type: object - properties: - id: - type: string - minLength: 8 - maxLength: 255 - pattern: "^[a-zA-Z0-9_]+$" - description: | - The unique identifier of this API within Unkey's system. - Used in all operations related to this API including key creation, verification, and management. - Always begins with 'api_' followed by alphanumeric characters and underscores. - This identifier is permanent and never changes after API creation. - example: api_1234567890abcdef - name: - type: string - minLength: 3 - maxLength: 255 - description: | - The internal name of this API as specified during creation. - Used for organization and identification within your workspace. - Helps distinguish between different environments, services, or access tiers. - Not visible to end users - this is purely for administrative purposes. - example: payment-service-production - required: - - id - - name - additionalProperties: false - V2ApisCreateApiResponseBody: - type: object - required: - - meta - - data - properties: - meta: - "$ref": "#/components/schemas/Meta" - data: - "$ref": "#/components/schemas/ApisCreateApiResponseData" - additionalProperties: false - V2ApisDeleteApiResponseBody: - type: object - required: - - meta - properties: - meta: - "$ref": "#/components/schemas/Meta" - additionalProperties: false - V2ApisGetApiResponseBody: - type: object - required: - - meta - - data - properties: - meta: - "$ref": "#/components/schemas/Meta" - data: - "$ref": "#/components/schemas/ApisGetApiResponseData" - additionalProperties: false - V2ApisListKeysRequestBody: - type: object - required: - - apiId - properties: - apiId: - type: string - minLength: 1 - description: - The ID of the API whose keys you want to list (begins with - 'api_'). This endpoint returns all keys associated with this specific - API, subject to pagination and any additional filters provided. - example: api_1234 - limit: - type: integer - description: - The maximum number of keys to return in a single request. Use - this to control response size and loading performance. Higher values return - more keys but may increase response time. Lower values may require more - pagination requests but provide faster initial loading. - default: 100 - minimum: 1 - maximum: 100 - cursor: - type: string - description: - Pagination cursor from a previous response. Use this to fetch - subsequent pages of results when the response contains hasMore=true. Each - response containing additional results will include a new cursor value - in the pagination object. - example: cursor_eyJsYXN0S2V5SWQiOiJrZXlfMjNld3MiLCJsYXN0Q3JlYXRlZEF0IjoxNjcyNTI0MjM0MDAwfQ== - externalId: - type: string - minLength: 3 - description: - Optional filter to return only keys associated with a specific - external ID. This is useful when you need to find all keys belonging to - a particular user, organization, or entity in your system. The value must - exactly match the externalId set during key creation. - example: user_5bf93ab218e - decrypt: - type: boolean - description: |- - When true, attempts to include the plaintext key value in the response. SECURITY WARNING: - - This requires special permissions on the calling root key - - Only works for keys created with 'recoverable: true' - - Exposes sensitive key material in the response - - Should only be used in secure administrative contexts - - Never enable this in user-facing applications - default: false - revalidateKeysCache: - type: boolean - default: false - description: |- - EXPERIMENTAL: Skip the cache and fetch the keys directly from the database. This ensures you see the most recent state, including keys created moments ago. Use this when: - - You've just created a key and need to display it immediately - - You need absolute certainty about the current key state - - You're debugging cache consistency issues - - This parameter comes with a performance cost and should be used sparingly. - additionalProperties: false - ApisListKeysResponseData: - type: array - maxItems: 100 # DoS protection matching request limit - items: - "$ref": "#/components/schemas/KeyResponseData" - description: Array of API keys with complete configuration and metadata. - V2ApisListKeysResponseBody: - type: object - required: - - meta - - data - properties: - meta: - "$ref": "#/components/schemas/Meta" - data: - "$ref": "#/components/schemas/ApisListKeysResponseData" - pagination: - "$ref": "#/components/schemas/Pagination" - additionalProperties: false - RatelimitOverride: - type: object - additionalProperties: false - properties: - namespaceId: - description: - The unique identifier of the rate limit namespace this override - belongs to. This links the override to a specific namespace context, ensuring - the override only applies within that namespace. - type: string - minLength: 1 - maxLength: 255 - overrideId: - description: - The unique identifier of this specific rate limit override. - This ID is generated when the override is created and can be used for - management operations like updating or deleting the override. - type: string - minLength: 1 - maxLength: 255 - duration: - description: - The duration in milliseconds for this override's rate limit - window. This may differ from the default duration for the namespace, allowing - custom time windows for specific entities. After this duration elapses, - the rate limit counter for affected identifiers resets to zero. - format: int64 - type: integer - minimum: 1000 - identifier: - description: |- - The identifier pattern this override applies to. This determines which entities receive the custom rate limit. - - This can be: - - An exact identifier for a specific entity - - A pattern with wildcards for matching multiple entities - - Wildcard examples: - - 'admin_*' matches any identifier starting with 'admin_' - - '*_test' matches any identifier ending with '_test' - - '*premium*' matches any identifier containing 'premium' - - More complex patterns can combine multiple wildcards. Detailed documentation on pattern matching rules is available at https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules - type: string - minLength: 1 - maxLength: 255 - limit: - description: |- - The maximum number of requests allowed for entities matching this override. This replaces the default limit for the namespace when applied. - - Common use cases: - - Higher limits for premium customers - - Reduced limits for abusive or suspicious entities - - Zero limit to completely block specific patterns - - Custom tier-based limits for different customer segments - format: int64 - type: integer - minimum: 0 - required: - - namespaceId - - overrideId - - duration - - identifier - - limit - V2KeysVerifyKeyRequestBody: - type: object - required: - - apiId - - key - properties: - apiId: - type: string - minLength: 3 - maxLength: 255 # Reasonable upper bound for database identifiers - pattern: "^[a-zA-Z0-9_]+$" - description: | - Specifies which API the key belongs to for complete environment isolation. - Keys from different APIs cannot access each other, preventing cross-environment leaks. - Essential for ensuring development keys cannot access production data and vice versa. - Store this API ID in your service configuration rather than hardcoding it. - example: api_1234 - key: - type: string - minLength: 1 - maxLength: 512 # Reasonable upper bound for API key strings - description: | - The complete API key string provided by your user, including any prefix. - Verification uses secure hashing algorithms without storing plaintext values. - Never log, cache, or store API keys in your system as they provide full access to user resources. - Include the full key exactly as provided - even minor modifications will cause verification failure. - example: prefix_f4cc2d765275c206b7d76ff0e92e583067c4e33603fb4055d7ba3031cd7ce36a - tags: - type: array - items: - type: string - minLength: 1 - maxLength: 128 # Keep individual tags concise for analytics performance - pattern: "^[a-zA-Z0-9_=/.:-]+$" - maxItems: 20 # Allow sufficient tags for detailed analytics without performance impact - description: | - Attaches metadata tags for analytics and monitoring without affecting verification outcomes. - Enables segmentation of API usage in dashboards by endpoint, client version, region, or custom dimensions. - Use 'key=value' format for compatibility with most analytics tools and clear categorization. - Avoid including sensitive data in tags as they may appear in logs and analytics reports. - example: - - endpoint=/users/profile - - method=GET - - region=us-east-1 - - clientVersion=2.3.0 - - feature=premium - permissions: - type: string - minLength: 1 - maxLength: 1000 # Allow for complex permission queries - pattern: "^[a-zA-Z0-9_.()\\s-]+$" - description: | - Checks if the key has the specified permission(s) using a query syntax. - Supports single permissions, logical operators (AND, OR), and parentheses for grouping. - Examples: - - Single permission: "documents.read" - - Multiple permissions: "documents.read AND documents.write" - - Complex queries: "(documents.read OR documents.write) AND users.view" - Verification fails if the key lacks the required permissions through direct assignment or role inheritance. - example: "documents.read AND users.view" - credits: - "$ref": "#/components/schemas/KeysVerifyKeyCredits" - ratelimits: - type: array - items: - "$ref": "#/components/schemas/KeysVerifyKeyRatelimit" - description: | - Enforces time-based rate limiting during verification to prevent abuse and ensure fair usage. - Omitting this field skips rate limit checks entirely, relying only on configured key rate limits. - Multiple rate limits can be checked simultaneously, each with different costs and temporary overrides. - Rate limit checks are optimized for performance but may allow brief bursts during high concurrency. - additionalProperties: false - KeysVerifyKeyCredits: - type: object - required: - - cost - properties: - cost: - type: integer - format: int32 - minimum: 0 - maximum: 1000000000 - description: | - Sets how many credits to deduct for this verification request. - Use 0 for read-only operations or free tier access, higher values for premium features. - Credits are deducted immediately upon verification, even if the key lacks required permissions. - Essential for implementing usage-based pricing with different operation costs. - example: 5 - additionalProperties: false - description: | - Controls credit consumption for usage-based billing and quota enforcement. - Omitting this field uses the default cost of 1 credit per verification. - Credits provide globally consistent usage tracking, essential for paid APIs with strict quotas. - Verification can succeed while credit deduction fails if the key has insufficient credits. - KeysVerifyKeyRatelimit: - type: object - required: - - name - properties: - name: - type: string - minLength: 3 - maxLength: 255 # Reasonable upper bound for database identifiers - pattern: "" - description: References an existing ratelimit by its name. Key Ratelimits will take precedence over identifier-based limits. - example: tokens - cost: - type: integer - minimum: 0 - default: 1 - description: Optionally override how expensive this operation is and how many tokens are deducted from the current limit. - example: 2 - limit: - type: integer - minimum: 0 - description: Optionally override the maximum number of requests allowed within the specified interval. - example: 50 - duration: - type: integer - minimum: 0 - description: Optionally override the duration of the rate limit window duration. - example: 600000 - KeysVerifyKeyResponseData: - type: object - properties: - valid: - type: boolean - description: - The primary verification result. If true, the key is valid - and can be used. If false, check the 'code' field to understand why verification - failed. Your application should always check this field first before proceeding. - code: - type: string - enum: - - VALID - - NOT_FOUND - - FORBIDDEN - - USAGE_EXCEEDED - - RATE_LIMITED - - DISABLED - - INSUFFICIENT_PERMISSIONS - - EXPIRED - description: - "A machine-readable code indicating the verification status - or failure reason. Values: `VALID` (key is valid), `NOT_FOUND` (key doesn't - exist), `FORBIDDEN` (key exists but belongs to a different API), `USAGE_EXCEEDED` - (key has no more credits), `RATE_LIMITED` (key exceeded rate limits), `UNAUTHORIZED` - (key can't be used for this action), `DISABLED` (key was explicitly disabled), - `INSUFFICIENT_PERMISSIONS` (key lacks required permissions), `EXPIRED` (key - has passed its expiration date)." - keyId: - type: string - description: - The unique identifier of the verified key in Unkey's system. - Use this ID for operations like updating or revoking the key. This field - is returned for both valid and invalid keys (except when `code=NOT_FOUND`). - name: - type: string - description: - The human-readable name assigned to this key during creation. - This is useful for displaying in logs or admin interfaces to identify - the key's purpose or owner. - meta: - type: object - additionalProperties: true - description: - Custom metadata associated with the key. This can include any - JSON-serializable data you stored with the key during creation or updates, - such as plan information, feature flags, or user details. Use this to - avoid additional database lookups for contextual information needed during - API calls. - expires: - type: integer - format: int64 - description: - Unix timestamp (in milliseconds) when the key will expire. - If null or not present, the key has no expiration. You can use this to - warn users about upcoming expirations or to understand the validity period. - credits: - type: integer - format: int32 - description: - The number of requests/credits remaining for this key. If null - or not present, the key has unlimited usage. This value decreases with - each verification (based on the 'cost' parameter) unless explicit credit - refills are configured. - enabled: - type: boolean - description: - Indicates if the key is currently enabled. Disabled keys will - always fail verification with `code=DISABLED`. This is useful for implementing - temporary suspensions without deleting the key. - permissions: - type: array - items: - type: string - description: - A list of all permission names assigned to this key, either - directly or through roles. These permissions determine what actions the - key can perform. Only returned when permissions were checked during verification - or when the key fails with `code=INSUFFICIENT_PERMISSIONS`. - roles: - type: array - items: - type: string - description: - A list of all role names assigned to this key. Roles are collections - of permissions that grant access to specific functionality. Only returned - when permissions were checked during verification. - identity: - "$ref": "#/components/schemas/Identity" - description: - Information about the identity associated with this key. Identities - allow multiple keys to share resources (like rate limits) and represent - the same user or entity across different applications or devices. - ratelimits: - type: array - items: - "$ref": "#/components/schemas/VerifyKeyRatelimitData" - description: The ratelimits that got checked - required: - - valid - - code - VerifyKeyRatelimitData: - type: object - properties: - exceeded: - type: boolean - description: Whether the rate limit was exceeded. - id: - type: string - minLength: 8 - maxLength: 255 - pattern: "^rl_[a-zA-Z0-9_]+$" - description: Unique identifier for this rate limit configuration. - example: rl_1234567890abcdef - name: - type: string - minLength: 1 - maxLength: 128 - pattern: "^[a-zA-Z][a-zA-Z0-9_-]*$" - description: Human-readable name for this rate limit. - example: api_requests - limit: - type: integer - format: int64 - minimum: 1 - maximum: 1000000 - description: Maximum requests allowed within the time window. - example: 1000 - duration: - type: integer - format: int64 - minimum: 1000 - maximum: 2592000000 - description: Rate limit window duration in milliseconds. - example: 3600000 - reset: - type: integer - format: int64 - minimum: 1000 - maximum: 2592000000 - description: Rate limit reset duration in milliseconds. - example: 3600000 - remaining: - type: integer - format: int64 - minimum: 0 - maximum: 1000000 - description: Rate limit remaining requests within the time window. - example: 999 - autoApply: - type: boolean - description: | - Whether this rate limit should be automatically applied when verifying keys. - When true, we will automatically apply this limit during verification without it being explicitly listed. - example: true - required: - - id - - exceeded - - name - - limit - - duration - - reset - - remaining - - autoApply - additionalProperties: false - V2IdentitiesUpdateIdentityRequestBody: - type: object - properties: - externalId: - type: string - minLength: 3 - maxLength: 255 # Match database varchar limits for external identifiers - pattern: "^[a-zA-Z0-9_.-]+$" - description: | - Specifies which identity to update using your system's identifier from identity creation. - Use this when you track identities by your own user IDs, organization IDs, or tenant identifiers. - Accepts letters, numbers, underscores, dots, and hyphens for flexible identifier formats. - example: user_abc123 - meta: - type: object - additionalProperties: true - maxProperties: 100 # Prevent DoS while allowing rich metadata - description: | - Replaces all existing metadata with this new metadata object. - Omitting this field preserves existing metadata, while providing an empty object clears all metadata. - Avoid storing sensitive data here as it's returned in verification responses. - Large metadata objects increase verification latency and should stay under 10KB total size. - example: - name: Alice Smith - email: alice@example.com - plan: premium - ratelimits: - type: array - maxItems: 50 # Reasonable limit for rate limit configurations per identity - items: - "$ref": "#/components/schemas/RatelimitRequest" - description: | - Replaces all existing identity rate limits with this complete list of rate limits. - Omitting this field preserves existing rate limits, while providing an empty array removes all rate limits. - These limits are shared across all keys belonging to this identity, preventing abuse through multiple keys. - Rate limit changes take effect immediately but may take up to 30 seconds to propagate across all regions. - example: - - name: requests - limit: 1000 - duration: 3600000 - additionalProperties: false - required: - - externalId - IdentitiesUpdateIdentityResponseData: - type: object - required: - - externalId - properties: - externalId: - type: string - description: The external identifier for this identity in your system. - example: user_abc123 - meta: - type: object - additionalProperties: true - description: Custom metadata associated with this identity after the update. - example: - name: Alice Smith - email: alice@example.com - plan: premium - ratelimits: - type: array - items: - "$ref": "#/components/schemas/RatelimitResponse" - description: Rate limits associated with this identity after the update. - example: - - name: requests - limit: 1000 - duration: 3600000 - V2IdentitiesUpdateIdentityResponseBody: - type: object - required: - - data - - meta - properties: - data: - "$ref": "#/components/schemas/IdentitiesUpdateIdentityResponseData" - meta: - "$ref": "#/components/schemas/Meta" -paths: - "/v2/keys.setPermissions": - post: - tags: - - keys - summary: Set (replace) all permissions on an API key - description: |- - Sets the permissions for an existing API key by replacing all existing direct permissions with the provided set. This is a complete replacement operation - permissions not specified in the request will be removed. - - Use this endpoint when you want to: - - Synchronize API key permissions with an external system - - Reset a key's permissions to a known state - - Apply a standardized permission template to a key - - Remove all permissions from a key (by providing an empty array) - - Fix over-permissioned keys by applying the precise set needed - - Key differences from other endpoints: - - Unlike addPermissions, this replaces all permissions instead of just adding - - Unlike removePermissions, this sets the complete state rather than removing specific permissions - - The advantage is atomic replacement in a single operation versus multiple incremental changes - - Only direct permissions are affected - permissions granted through roles remain unchanged. Changes take effect immediately for new verifications, though existing authorized sessions may continue until their cache expires (typically under 30 seconds). - operationId: setPermissions - x-speakeasy-name-override: setPermissions - security: - - rootKey: [] - requestBody: - required: true - content: - application/json: - schema: - "$ref": "#/components/schemas/V2KeysSetPermissionsRequestBody" - examples: - removeAll: - summary: Remove all permissions from key - description: - This example won't work as expected! The permissions - array must contain at least one permission to remove. To remove - all permissions, use the setPermissions endpoint with an empty permissions - array instead. - value: - keyId: key_2cGKbMxRyIzhCxo1Idjz8q - permissions: [] - basic: - summary: Set permissions using IDs - description: - Using permission IDs is the most precise approach for - setting permissions, especially in automation scripts where exact - references are important. - value: - keyId: key_2cGKbMxRyIzhCxo1Idjz8q - permissions: - - id: perm_1n9McEIBSqy44Qy7hzWyM5 - - id: perm_2zF4mNyP9BsRj2aQwDxVkT - withNames: - summary: Add permissions using slugs - description: - Using permission slugs is more readable and maintainable. - Slugs must be unique within your workspace. - value: - keyId: key_2cGKbMxRyIzhCxo1Idjz8q - permissions: - - slug: documents.write - - slug: documents.delete - withCreation: - summary: Set with permission creation - description: - This example demonstrates setting permissions while simultaneously - creating new ones that don't exist yet. Requires the `rbac.*.create_permission` - permission on your root key. - value: - keyId: key_2cGKbMxRyIzhCxo1Idjz8q - permissions: - - slug: documents.write - - slug: reports.export - create: true - - name: reports.schedule - create: true - mixed: - summary: Mix of ID and name references - description: - You can combine different reference methods in a single - request - some permissions by ID, others by name, and even create - new ones on the fly. - value: - keyId: key_2cGKbMxRyIzhCxo1Idjz8q - permissions: - - id: perm_1n9McEIBSqy44Qy7hzWyM5 - - slug: documents.delete - - slug: reports.view - create: true - responses: - "200": - description: - Permissions successfully set on the key. The previous direct - permission set has been completely replaced with the new set specified - in the request. - content: - application/json: - schema: - "$ref": "#/components/schemas/V2KeysSetPermissionsResponse" - examples: - standard: - summary: Complete list of permissions - value: - meta: - requestId: req_2cGKbMxRyIzhCxo1Idjz8q - data: - - id: perm_1n9McEIBSqy44Qy7hzWyM5 - name: documents.write - - id: perm_2zF4mNyP9BsRj2aQwDxVkT - name: documents.delete - - id: perm_3qRsTu2vWxYzAbCdEfGhIj - name: reports.view - empty: - summary: All permissions removed - value: - meta: - requestId: req_3qRsTu2vWxYzAbCdEfGhIj - data: [] - "400": - description: - Bad Request - Invalid keyId format, missing required fields, - or malformed permission entries - content: - application/json: - schema: - "$ref": "#/components/schemas/BadRequestErrorResponse" - examples: - invalidKeyId: - summary: Invalid keyId format - value: - meta: - requestId: req_5zAbCdEfGhIjKlMnOpQrSt - error: - title: Bad Request - detail: keyId must start with 'key_' - status: 400 - type: bad_request - emptyPermissions: - summary: Empty permissions array - value: - meta: - requestId: req_6aBcDeFgHiJkLmNoPqRsT - error: - title: Bad Request - detail: At least one permission must be specified - status: 400 - type: bad_request - missingIdentifier: - summary: Permission missing both id and name - value: - meta: - requestId: req_7bCdEfGhIjKlMnOpQrStUv - error: - title: Bad Request - detail: Each permission must include either id or name - status: 400 - type: bad_request - "401": - description: Unauthorized - Missing or invalid authentication credentials - content: - application/json: - schema: - "$ref": "#/components/schemas/UnauthorizedErrorResponse" - examples: - invalidRootKey: - summary: Invalid root key provided - value: - meta: - requestId: req_9tUv3wXyZaAbCdEfGhIjKl - error: - title: Unauthorized - detail: The root key provided is invalid or has been revoked. - status: 401 - type: unauthorized - "403": - description: - Forbidden - Insufficient permissions (requires `rbac.*.add_permission_to_key` - and `rbac.*.remove_permission_from_key` and potentially `rbac.*.create_permission`) - content: - application/json: - schema: - "$ref": "#/components/schemas/ForbiddenErrorResponse" - examples: - missingAddPermission: - summary: Missing add permission - value: - meta: - requestId: req_0uVwX4yZaAbCdEfGhIjKl - error: - title: Forbidden - detail: - Your root key requires the 'rbac.*.add_permission_to_key' - permission to perform this operation - status: 403 - type: forbidden - missingRemovePermission: - summary: Missing remove permission - value: - meta: - requestId: req_1vWxYzAbCdEfGhIjKlMnOp - error: - title: Forbidden - detail: - Your root key requires the 'rbac.*.remove_permission_from_key' - permission to perform this operation - status: 403 - type: forbidden - missingCreatePermission: - summary: Cannot create new permissions - value: - meta: - requestId: req_4bVcWdXeYfZgHiJkLmNoPq - error: - title: Forbidden - detail: - Your root key requires the 'rbac.*.create_permission' - permission to create new permissions - status: 403 - type: forbidden - "404": - description: - Not Found - Key not found or specified permission IDs don't - exist - content: - application/json: - schema: - "$ref": "#/components/schemas/NotFoundErrorResponse" - examples: - keyNotFound: - summary: Key not found - value: - meta: - requestId: req_2wXyZaAbCdEfGhIjKlMnOp - error: - title: Not Found - detail: Key key_2cGKbMxRyIzhCxo1Idjz8q not found - status: 404 - type: not_found - permissionNotFound: - summary: Permission not found - value: - meta: - requestId: req_3xYzAbCdEfGhIjKlMnOpQr - error: - title: Not Found - detail: - Permission perm_1n9McEIBSqy44Qy7hzWyM5 not found and - not allowed to create - status: 404 - type: not_found - "500": - description: - Internal Server Error - An unexpected error occurred while - processing the request - content: - application/json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - examples: - databaseError: - summary: Database error - value: - meta: - requestId: req_4yZaAbCdEfGhIjKlMnOpQrS - error: - title: Internal Server Error - detail: - An unexpected error occurred while processing your request. - Please try again later. - status: 500 - type: internal_server_error - cachingError: - summary: Cache invalidation error - value: - meta: - requestId: req_5zAbCdEfGhIjKlMnOpQrSt - error: - title: Internal Server Error - detail: - The permissions were successfully set but there was - an error invalidating cached keys. Some systems may temporarily - see stale data. - status: 500 - type: internal_server_error - "/v2/keys.removePermissions": - post: - tags: - - keys - summary: Remove permissions from an API key - description: |- - Removes one or more permissions from an existing API key. This endpoint is used to selectively revoke access rights from a key without deleting it or affecting other permissions. - - Key features: - - Selective removal - revoke specific permissions while leaving others intact - - Direct permissions only - doesn't affect permissions granted through roles - - Idempotent operation - removing permissions multiple times has no additional effect - - Atomic transaction - all permissions are removed or none are (rollback on failure) - - Immediate effect - new verifications will see changes within seconds - - Cache invalidation - all regions eventually reflect the changes - - Use cases: - - Downgrading user access privileges - - Removing temporary elevated permissions - - Implementing granular permission adjustments - - Revoking access to specific resources - - This endpoint complements addPermissions (for granting) and setPermissions (for complete replacement). Use this when you need to selectively remove capabilities without altering other permissions. - operationId: removePermissions - x-speakeasy-name-override: removePermissions - security: - - rootKey: [] - requestBody: - required: true - content: - application/json: - schema: - "$ref": "#/components/schemas/V2KeysRemovePermissionsRequestBody" - examples: - removeAll: - summary: Remove all permissions from key - description: - Setting an empty permissions array removes all direct - permissions from the key. This doesn't remove permissions granted - through roles. The key remains valid but will have no direct permissions. - value: - keyId: key_2cGKbMxRyIzhCxo1Idjz8q - permissions: [] - basic: - summary: Remove permissions using IDs - description: - Using permission IDs is the most precise way to remove - permissions, guaranteeing you're removing exactly what you intend - regardless of name changes. - value: - keyId: key_2cGKbMxRyIzhCxo1Idjz8q - permissions: - - id: perm_1n9McEIBSqy44Qy7hzWyM5 - - id: perm_2zF4mNyP9BsRj2aQwDxVkT - withNames: - summary: Remove permissions using names - description: - Using permission names is more human-readable but requires - exact name matches, including full path and correct case sensitivity. - value: - keyId: key_2cGKbMxRyIzhCxo1Idjz8q - permissions: - - name: documents.write - - name: documents.delete - mixed: - summary: Mix of ID and name references - description: - You can combine ID-based and name-based references in - a single request. This is useful when you have exact IDs for some - permissions but only names for others. - value: - keyId: key_2cGKbMxRyIzhCxo1Idjz8q - permissions: - - id: perm_1n9McEIBSqy44Qy7hzWyM5 - - name: reports.export - removeAccessGroup: - summary: Remove all permissions for a resource group - description: - A common pattern is removing all permissions related - to a specific resource (e.g., 'documents') when revoking access - to that resource type. - value: - keyId: key_2cGKbMxRyIzhCxo1Idjz8q - permissions: - - name: documents.read - - name: documents.write - - name: documents.delete - - name: documents.share - responses: - "200": - description: - Permissions successfully removed from the key. All requested - permissions have been removed if they were present. Any permissions that - weren't assigned to the key were simply ignored without causing an error. - content: - application/json: - schema: - "$ref": "#/components/schemas/V2KeysRemovePermissionsResponse" - examples: - standard: - summary: Successful removal - description: - The response body contains only metadata and an empty - data object. This minimalist response structure is by design - - if you receive a 200 status code, all requested permissions have - been successfully removed (or weren't present to begin with). - value: - meta: - requestId: req_2cGKbMxRyIzhCxo1Idjz8q - data: {} - "400": - description: - Bad Request - Invalid keyId format, missing required fields, - or malformed permission entries - content: - application/json: - schema: - "$ref": "#/components/schemas/BadRequestErrorResponse" - examples: - invalidKeyId: - summary: Invalid keyId format - value: - meta: - requestId: req_5zAbCdEfGhIjKlMnOpQrSt - error: - title: Bad Request - detail: keyId must start with 'key_' - status: 400 - type: bad_request - emptyPermissions: - summary: Empty permissions array - value: - meta: - requestId: req_6aBcDeFgHiJkLmNoPqRsT - error: - title: Bad Request - detail: At least one permission must be specified - status: 400 - type: bad_request - missingIdentifier: - summary: Permission missing both id and name - value: - meta: - requestId: req_7bCdEfGhIjKlMnOpQrStUv - error: - title: Bad Request - detail: Each permission must include either id or name - status: 400 - type: bad_request - "401": - description: Unauthorized - Missing or invalid authentication credentials - content: - application/json: - schema: - "$ref": "#/components/schemas/UnauthorizedErrorResponse" - examples: - invalidRootKey: - summary: Invalid root key provided - value: - meta: - requestId: req_9tUv3wXyZaAbCdEfGhIjKl - error: - title: Unauthorized - detail: The root key provided is invalid or has been revoked. - status: 401 - type: unauthorized - "403": - description: Forbidden - Insufficient permissions (requires `rbac.*.remove_permission_from_key`) - content: - application/json: - schema: - "$ref": "#/components/schemas/ForbiddenErrorResponse" - examples: - missingPermission: - summary: Missing required permission - value: - meta: - requestId: req_0uVwX4yZaAbCdEfGhIjKl - error: - title: Forbidden - detail: - Your root key requires the 'rbac.*.remove_permission_from_key' - permission to perform this operation - status: 403 - type: forbidden - "404": - description: Not Found - Key not found - content: - application/json: - schema: - "$ref": "#/components/schemas/NotFoundErrorResponse" - examples: - keyNotFound: - summary: Key not found - value: - meta: - requestId: req_2wXyZaAbCdEfGhIjKlMnOp - error: - title: Not Found - detail: Key key_2cGKbMxRyIzhCxo1Idjz8q not found - status: 404 - type: not_found - "500": - description: - Internal Server Error - An unexpected error occurred while - processing the request - content: - application/json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - examples: - databaseError: - summary: Database error - value: - meta: - requestId: req_4yZaAbCdEfGhIjKlMnOpQrS - error: - title: Internal Server Error - detail: - An unexpected error occurred while processing your request. - Please try again later. - status: 500 - type: internal_server_error - cachingError: - summary: Cache invalidation error - value: - meta: - requestId: req_5zAbCdEfGhIjKlMnOpQrSt - error: - title: Internal Server Error - detail: - The permissions were successfully removed but there - was an error invalidating cached keys. Some systems may temporarily - see stale data. - status: 500 - type: internal_server_error - "/v2/keys.addRoles": - post: - tags: - - keys - summary: Add roles to an API key - description: |- - Assigns one or more roles to an existing API key, incrementally adding to any existing roles already assigned. Roles are collections of permissions that provide a convenient way to assign multiple permissions at once rather than assigning individual permissions one by one. - - This operation is idempotent, meaning adding the same role multiple times has no additional effect. The assignment happens as an atomic transaction where all roles are added or none are with rollback on failure. Changes take effect immediately for new verifications within seconds, and during verification, permissions from all assigned roles are combined. - - Use this endpoint when promoting users to higher access levels, adding specialized function access to existing keys, or implementing role-based access control patterns. It provides a standardized way to bundle permissions across many keys while preserving existing role assignments. Unlike setRoles which replaces all roles, this endpoint only adds to the existing set of roles assigned to a key. - operationId: addRoles - x-speakeasy-name-override: addRoles - security: - - rootKey: [] - requestBody: - required: true - content: - application/json: - schema: - "$ref": "#/components/schemas/V2KeysAddRolesRequestBody" - examples: - basic: - summary: Adding roles by ID - description: - Adding roles using their IDs is the most precise method, - ensuring you're adding exactly the intended roles regardless of - name changes. - value: - keyId: key_2cGKbMxRyIzhCxo1Idjz8q - roles: - - id: role_1n9McEIBSqy44Qy7hzWyM5 - - id: role_2a8NdFJCTrz55Ry8Jdkz9r - withNames: - summary: Adding roles by name - description: - Adding roles by name is more human-readable and convenient - when working with well-known role names in your system. - value: - keyId: key_2cGKbMxRyIzhCxo1Idjz8q - roles: - - name: admin - - name: billing_manager - mixed: - summary: Adding roles by mixed identifiers - description: - You can mix both ID and name references in a single request, - using whichever is more convenient for each role. - value: - keyId: key_2cGKbMxRyIzhCxo1Idjz8q - roles: - - id: role_1n9McEIBSqy44Qy7hzWyM5 - - name: billing_manager - responses: - "200": - description: Roles successfully added to the key - content: - application/json: - schema: - "$ref": "#/components/schemas/V2KeysAddRolesResponse" - examples: - standard: - summary: Complete list of roles - value: - meta: - requestId: req_1234567890abcdef - data: - - id: role_1n9McEIBSqy44Qy7hzWyM5 - name: admin - - id: role_2a8NdFJCTrz55Ry8Jdkz9r - name: billing_manager - - id: role_3b9OeGKDUsy66Sz9Kelz0s - name: developer - empty: - summary: No roles assigned - value: - meta: - requestId: req_1234567890abcdef - data: [] - "400": - description: Bad request - Invalid parameters or configuration - content: - application/json: - schema: - "$ref": "#/components/schemas/BadRequestErrorResponse" - examples: - invalidPrefix: - summary: Invalid prefix format - value: - meta: - requestId: req_01H9TQPP77V5E48E9SH0BG0ZQX - error: - title: Bad Request - detail: - Prefix must contain only alphanumeric characters, underscores, - and hyphens - status: 400 - type: https://unkey.dev/errors/bad-request - errors: - - message: - Prefix must contain only alphanumeric characters, - underscores, and hyphens - location: body.prefix - "401": - description: Unauthorized - content: - application/json: - schema: - "$ref": "#/components/schemas/UnauthorizedErrorResponse" - "403": - description: Forbidden - content: - application/json: - schema: - "$ref": "#/components/schemas/ForbiddenErrorResponse" - "404": - description: Not found - Key doesn't exist or was deleted - content: - application/json: - schema: - "$ref": "#/components/schemas/NotFoundErrorResponse" - "500": - description: Internal server error - content: - application/json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - "/v2/keys.addPermissions": - post: - tags: - - keys - summary: Add permissions to an API key - description: |- - Assigns one or more permissions to an existing API key. This endpoint is used to incrementally add permissions without removing existing ones. - - Permissions define access control capabilities that are validated during key verification. Each permission typically follows a 'resource.action' naming pattern (e.g., 'documents.read', 'users.create'). A key can have both direct permissions (added via this endpoint) and indirect permissions (granted through roles). - - Key features: - - Idempotent operation - adding the same permission multiple times has no additional effect - - Atomic transaction - all permissions are added or none are (rollback on failure) - - Permission creation - optionally create new permissions if they don't exist (with 'create: true') - - Cache invalidation - changes take effect immediately for new verifications - - Hierarchical permissions - during verification, 'documents.*' grants access to both 'documents.read' and 'documents.write' - - Unlike setPermissions (which replaces all permissions), this endpoint only adds to the existing set. Use this when incrementally granting new capabilities to existing keys. - operationId: addPermissions - x-speakeasy-name-override: addPermissions - security: - - rootKey: [] - requestBody: - required: true - content: - application/json: - schema: - "$ref": "#/components/schemas/V2KeysAddPermissionsRequestBody" - examples: - basic: - summary: Add permissions using IDs - description: - When you know the exact permission IDs, referencing them - directly is the most precise approach. Permission IDs are guaranteed - to be unique. - value: - keyId: key_2cGKbMxRyIzhCxo1Idjz8q - permissions: - - id: perm_1n9McEIBSqy44Qy7hzWyM5 - - id: perm_2zF4mNyP9BsRj2aQwDxVkT - withNames: - summary: Add permissions using names - value: - keyId: key_2cGKbMxRyIzhCxo1Idjz8q - permissions: - - name: documents.write - - name: documents.delete - withCreation: - summary: Add and create new permissions - description: - Setting create=true dynamically creates permissions if - they don't exist yet. This requires the `rbac.*.create_permission` - permission on your root key. - value: - keyId: key_2cGKbMxRyIzhCxo1Idjz8q - permissions: - - name: reports.export - create: true - - name: reports.schedule - create: true - mixed: - summary: Mix ID and name references - description: - You can combine ID references, name references, and creation - in a single request. If both id and name are provided for any permission, - the id takes precedence. - value: - keyId: key_2cGKbMxRyIzhCxo1Idjz8q - permissions: - - id: perm_1n9McEIBSqy44Qy7hzWyM5 - - name: documents.publish - - name: analytics.view - create: true - hierarchical: - summary: Using hierarchical permission naming - description: - Permissions can use hierarchical naming with dots as - separators. During verification, a request for 'billing.invoices.*' - will match both 'billing.invoices.create' and 'billing.invoices.view'. - value: - keyId: key_2cGKbMxRyIzhCxo1Idjz8q - permissions: - - name: billing.invoices.create - create: true - - name: billing.invoices.view - create: true - - name: billing.payments.process - create: true - completeBilling: - summary: Complete billing system permissions - description: - Creating a structured set of permissions for a complete - subsystem like billing - value: - keyId: key_2cGKbMxRyIzhCxo1Idjz8q - permissions: - - name: billing.invoices.create - create: true - - name: billing.invoices.view - create: true - - name: billing.invoices.update - create: true - - name: billing.payments.process - create: true - - name: billing.payments.refund - create: true - - name: billing.settings.view - create: true - responses: - "200": - description: Permissions successfully added to the key - content: - application/json: - schema: - "$ref": "#/components/schemas/V2KeysAddPermissionsResponse" - examples: - standard: - summary: Complete list of permissions - description: - After adding new permissions, the response includes - the full list of all permissions now assigned to the key, sorted - alphabetically by name. - value: - meta: - requestId: req_2cGKbMxRyIzhCxo1Idjz8q - data: - - id: perm_1n9McEIBSqy44Qy7hzWyM5 - name: documents.write - - id: perm_2zF4mNyP9BsRj2aQwDxVkT - name: documents.delete - - id: perm_3qRsTu2vWxYzAbCdEfGhIj - name: documents.read - - id: perm_4bVcWdXeYfZgHiJkLmNoPq - name: reports.export - - id: perm_5sTu2vWxYzAbCdEfGhIjKl - name: reports.schedule - hierarchical: - summary: Key with hierarchical permissions - value: - meta: - requestId: req_7zF4mNyP9BsRj2aQwDxVkT - data: - - id: perm_1n9McEIBSqy44Qy7hzWyM5 - name: billing.invoices.create - - id: perm_2zF4mNyP9BsRj2aQwDxVkT - name: billing.invoices.view - - id: perm_3qRsTu2vWxYzAbCdEfGhIj - name: billing.payments.process - - id: perm_4bVcWdXeYfZgHiJkLmNoPq - name: billing.settings.read - - id: perm_5sTu2vWxYzAbCdEfGhIjKl - name: billing.settings.write - empty: - summary: Key with no permissions - value: - meta: - requestId: req_8sTu2vWxYzAbCdEfGhIjKl - data: [] - "400": - description: - Bad Request - Invalid keyId format, missing required fields, - or malformed permission entries - content: - application/json: - schema: - "$ref": "#/components/schemas/BadRequestErrorResponse" - examples: - invalidKeyId: - summary: Invalid keyId format - value: - meta: - requestId: req_5zAbCdEfGhIjKlMnOpQrSt - error: - title: Bad Request - detail: keyId must start with 'key_' - status: 400 - type: bad_request - emptyPermissions: - summary: Empty permissions array - value: - meta: - requestId: req_6aBcDeFgHiJkLmNoPqRsT - error: - title: Bad Request - detail: At least one permission must be specified - status: 400 - type: bad_request - "401": - description: Unauthorized - Missing or invalid authentication credentials - content: - application/json: - schema: - "$ref": "#/components/schemas/UnauthorizedErrorResponse" - examples: - invalidRootKey: - summary: Invalid root key provided - value: - meta: - requestId: req_9tUv3wXyZaAbCdEfGhIjKl - error: - title: Unauthorized - detail: The root key provided is invalid or has been revoked. - status: 401 - type: unauthorized - "403": - description: - Forbidden - Insufficient permissions (requires `rbac.*.add_permission_to_key` - and potentially `rbac.*.create_permission`) - content: - application/json: - schema: - "$ref": "#/components/schemas/ForbiddenErrorResponse" - examples: - missingPermission: - summary: Missing required permission - value: - meta: - requestId: req_0uVwX4yZaAbCdEfGhIjKl - error: - title: Forbidden - detail: - Your root key requires the 'rbac.*.add_permission_to_key' - permission to perform this operation - status: 403 - type: forbidden - missingCreatePermission: - summary: Cannot create new permissions - value: - meta: - requestId: req_1vWxYzAbCdEfGhIjKlMnOp - error: - title: Forbidden - detail: - Your root key requires the 'rbac.*.create_permission' - permission to create new permissions - status: 403 - type: forbidden - "404": - description: - Not Found - Key not found or specified permission IDs don't - exist - content: - application/json: - schema: - "$ref": "#/components/schemas/NotFoundErrorResponse" - examples: - keyNotFound: - summary: Key not found - value: - meta: - requestId: req_2wXyZaAbCdEfGhIjKlMnOp - error: - title: Not Found - detail: Key key_2cGKbMxRyIzhCxo1Idjz8q not found - status: 404 - type: not_found - permissionNotFound: - summary: Permission not found - value: - meta: - requestId: req_3xYzAbCdEfGhIjKlMnOpQr - error: - title: Not Found - detail: Permission perm_1n9McEIBSqy44Qy7hzWyM5 not found - status: 404 - type: not_found - "500": - description: - Internal Server Error - An unexpected error occurred while - processing the request - content: - application/json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - examples: - databaseError: - summary: Database error - value: - meta: - requestId: req_4yZaAbCdEfGhIjKlMnOpQrS - error: - title: Internal Server Error - detail: - An unexpected error occurred while processing your request. - Please try again later. - status: 500 - type: internal_server_error - cachingError: - summary: Cache invalidation error - value: - meta: - requestId: req_5zAbCdEfGhIjKlMnOpQrSt - error: - title: Internal Server Error - detail: - The permissions were successfully added but there was - an error invalidating cached keys. Some systems may temporarily - see stale data. - status: 500 - type: internal_server_error - "/v2/keys.removeRoles": - post: - tags: - - keys - summary: Remove roles from an API key - description: |- - Removes one or more roles from an existing API key, selectively revoking role-based access without affecting other assigned roles. When a role is removed, all permissions granted exclusively through that role are revoked, while permissions also granted through remaining roles or direct assignments remain accessible. - - The operation is idempotent, meaning removing roles multiple times has no additional effect. All specified roles are removed atomically or none are with rollback on failure. Changes take effect immediately for new verifications, though cached sessions may retain old permissions briefly (typically under 30 seconds) due to cache invalidation across all regions. - - This endpoint complements addRoles for granting access and setRoles for complete replacement. Use this when you need to selectively revoke specific roles without altering other role assignments, providing precise control over role-based access management. - operationId: removeRoles - x-speakeasy-name-override: removeRoles - security: - - rootKey: [] - requestBody: - required: true - content: - application/json: - schema: - "$ref": "#/components/schemas/V2KeysRemoveRolesRequestBody" - examples: - basic: - summary: Removing roles by ID - description: - Removing roles by their exact IDs ensures precision in - revoking the exact intended roles, regardless of name changes. - value: - keyId: key_2cGKbMxRyIzhCxo1Idjz8q - roles: - - id: role_1n9McEIBSqy44Qy7hzWyM5 - - id: role_2a8NdFJCTrz55Ry8Jdkz9r - withNames: - summary: Removing roles by name - description: - Removing roles by name is more human-readable but requires - exact name matches, including correct capitalization. - value: - keyId: key_2cGKbMxRyIzhCxo1Idjz8q - roles: - - name: admin - - name: billing_manager - mixed: - summary: Removing roles by mixed identifiers - description: - You can combine both ID and name references in a single - request to remove multiple roles using whichever reference method - is most convenient for each role. - value: - keyId: key_2cGKbMxRyIzhCxo1Idjz8q - roles: - - id: role_1n9McEIBSqy44Qy7hzWyM5 - - name: billing_manager - responses: - "200": - description: - Roles successfully removed from the key. The response shows - all remaining roles still assigned to the key after the removal operation. - content: - application/json: - schema: - "$ref": "#/components/schemas/V2KeysRemoveRolesResponse" - examples: - standard: - summary: Remaining roles after removal - value: - meta: - requestId: req_1234567890abcdef - data: - - id: role_3b9OeGKDUsy66Sz9Kelz0s - name: developer - empty: - summary: No roles remaining - value: - meta: - requestId: req_1234567890abcdef - data: [] - "400": - description: Bad request - Invalid parameters or configuration - content: - application/json: - schema: - "$ref": "#/components/schemas/BadRequestErrorResponse" - examples: - invalidKeyId: - summary: Invalid key ID format - value: - meta: - requestId: req_01H9TQPP77V5E48E9SH0BG0ZQZ - error: - title: Bad Request - detail: keyId must be a valid key ID starting with 'key_' - status: 400 - type: https://unkey.dev/errors/bad-request - errors: - - message: keyId must be a valid key ID starting with 'key_' - location: body.keyId - "401": - description: Unauthorized - content: - application/json: - schema: - "$ref": "#/components/schemas/UnauthorizedErrorResponse" - "403": - description: Forbidden - content: - application/json: - schema: - "$ref": "#/components/schemas/ForbiddenErrorResponse" - "404": - description: - Not found - The specified identity doesn't exist. This error - occurs when no identity matches the provided externalId. - This might happen if the identity was already deleted, never existed, - or if there's a typo in the identifier. Verify the identity exists before - attempting deletion. - content: - application/json: - schema: - "$ref": "#/components/schemas/NotFoundErrorResponse" - "500": - description: Internal server error - content: - application/json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - "/v2/keys.updateCredits": - post: - tags: - - keys - summary: Update remaining credits for an API key - description: | - Sets the remaining number of credits for a key, providing precise control over usage limits for implementing usage-based business models. Use this endpoint when customers make purchases to add credits, during subscription renewals to reset credits, or for adjustments due to refunds and promotions. - - The credits feature provides globally consistent usage limiting where each successful verification decrements the counter by the specified cost value (default 1). When credits reach zero, verification fails with `code=USAGE_EXCEEDED`, making this ideal for monetization models where accuracy is critical compared to rate limits which control frequency. - - This endpoint works alongside `createKey` for setting initial credits and the automatic refill system for periodic replenishment. You can optionally remove automatic refill settings from the key, effectively converting it from periodic refills to fixed credit allocation. Common scenarios include implementing free trial extensions, granting bonus credits as incentives, or converting between subscription models. - operationId: updateCredits - x-speakeasy-name-override: updateCredits - security: - - rootKey: [] - requestBody: - required: true - content: - application/json: - schema: - "$ref": "#/components/schemas/V2KeysUpdateCreditsRequestBody" - examples: - setUnlimited: - summary: Set unlimited usage - description: - Setting remaining to null makes the key usable an unlimited - number of times. This is useful for premium tiers, trusted partners, - or internal applications where usage counting isn't needed. Any previous refill settings will be removed. - value: - keyId: key_2cGKbMxRyIzhCxo1Idjz8q - value: null - operation: set - setCredits: - summary: Set credits - description: To set credits, first get the current value using keys.getKey, then add the amount. - value: - keyId: key_2cGKbMxRyIzhCxo1Idjz8q - value: 2500 - operation: set - incrementCredits: - summary: Increment credits - description: To increment credits to the existing balance, just specify the increment operation and the amount to add. - value: - keyId: key_2cGKbMxRyIzhCxo1Idjz8q - value: 2500 - operation: increment - decrementCredits: - summary: Decrement credits - description: To decrement credits to the existing balance, just specify the decrement operation and the amount to subtract. - value: - keyId: key_2cGKbMxRyIzhCxo1Idjz8q - value: 2500 - operation: decrement - responses: - "200": - description: Remaining credits successfully updated - content: - application/json: - schema: - "$ref": "#/components/schemas/V2KeysUpdateCreditsResponse" - examples: - withRefills: - summary: Updated with refill settings preserved - value: - meta: - requestId: req_1234567890abcdef - data: - remaining: 1000 - refillSettings: - interval: monthly - amount: 1000 - refillDay: 1 - withoutRefills: - summary: Updated with refills removed - value: - meta: - requestId: req_1234567890abcdef - data: - remaining: 500 - refillSettings: - unlimited: - summary: Set to unlimited usage - value: - meta: - requestId: req_1234567890abcdef - data: - remaining: null - refillSettings: - "400": - description: - Bad request - Invalid or missing parameters. This error occurs - when the request body is malformed, missing required fields, or contains - invalid values. Verify that you've provided a valid externalId, and that the format of each field meets the requirements. - content: - application/json: - schema: - "$ref": "#/components/schemas/BadRequestErrorResponse" - "401": - description: Unauthorized - content: - application/json: - schema: - "$ref": "#/components/schemas/UnauthorizedErrorResponse" - "403": - description: Forbidden - content: - application/json: - schema: - "$ref": "#/components/schemas/ForbiddenErrorResponse" - "404": - description: Not found - content: - application/json: - schema: - "$ref": "#/components/schemas/NotFoundErrorResponse" - "500": - description: Internal server error - content: - application/json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - "/v2/keys.setRoles": - post: - tags: - - keys - summary: Set roles for an API key - description: |- - Replaces all existing direct role assignments on a key with the provided set of roles. This endpoint offers complete control over a key's role assignments in a single operation. - - Unlike incremental endpoints like addRoles or removeRoles, setRoles performs a wholesale replacement that completely resets a key's roles to the specified set. This makes it ideal for synchronizing key roles with external systems, applying standardized role templates to keys, or implementing role-based access control with predictable outcomes. - - All existing direct role assignments not included in the request will be removed, making this a complete replacement operation rather than an incremental update. The operation is atomic where all roles are set or none are with rollback on failure, and changes take effect immediately for new verifications with all role changes logged in the audit log for security tracking. - - After roles are set, cached versions of the key are immediately invalidated to ensure consistency, though existing verification sessions may retain old roles briefly (typically under 30 seconds). Only direct role assignments are affected, not roles granted through other mechanisms. - operationId: setRoles - x-speakeasy-name-override: setRoles - security: - - rootKey: [] - requestBody: - required: true - content: - application/json: - schema: - "$ref": "#/components/schemas/V2KeysSetRolesRequestBody" - examples: - standard: - summary: Setting multiple roles - description: - This example shows setting a mix of roles by both ID - and name. After this operation, the key will have exactly these - two roles - any other roles previously assigned will be removed. - value: - keyId: key_2cGKbMxRyIzhCxo1Idjz8q - roles: - - id: role_1n9McEIBSqy44Qy7hzWyM5 - - name: developer - removeAll: - summary: Removing all roles - description: - By providing an empty roles array, you can remove all - direct role assignments from a key. This doesn't affect the key's - validity, but removes all role-based permissions. - value: - keyId: key_2cGKbMxRyIzhCxo1Idjz8q - roles: [] - standardTemplate: - summary: Applying a standard role template - description: - A common pattern is applying a standard set of roles - to maintain consistency across keys for users with similar access - needs. - value: - keyId: key_2cGKbMxRyIzhCxo1Idjz8q - roles: - - name: basic_user - - name: analytics_viewer - - name: self_service - responses: - "200": - description: Roles successfully set for the key - content: - application/json: - schema: - "$ref": "#/components/schemas/V2KeysSetRolesResponse" - examples: - standard: - summary: Roles after update - value: - meta: - requestId: req_1234567890abcdef - data: - - id: role_1n9McEIBSqy44Qy7hzWyM5 - name: admin - - id: role_3b9OeGKDUsy66Sz9Kelz0s - name: developer - empty: - summary: All roles removed - value: - meta: - requestId: req_1234567890abcdef - data: [] - "400": - description: Bad request - content: - application/json: - schema: - "$ref": "#/components/schemas/BadRequestErrorResponse" - "401": - description: Unauthorized - content: - application/json: - schema: - "$ref": "#/components/schemas/UnauthorizedErrorResponse" - "403": - description: Forbidden - content: - application/json: - schema: - "$ref": "#/components/schemas/ForbiddenErrorResponse" - "404": - description: Not found - content: - application/json: - schema: - "$ref": "#/components/schemas/NotFoundErrorResponse" - "500": - description: Internal server error - content: - application/json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - "/v2/keys.updateKey": - post: - tags: - - keys - summary: Update API key properties without changing the key itself - description: |- - Modifies the properties of an existing API key without changing the key string itself. This endpoint supports partial updates - you only need to include the fields you want to change. - - You can use this endpoint to: - - Rename keys for better organization - - Update metadata to reflect changes in user status, plan, or properties - - Modify usage limits or rate limits - - Enable or disable keys temporarily - - Change key expiration dates - - Add or update user identifiers - - To explicitly remove/disable a feature, set its field to null. Fields not included in the request remain unchanged. For managing permissions or roles, use the specialized endpoints (keys.addPermissions, keys.removePermissions, etc.) instead. - - Changes may take up to 30 seconds to propagate to all regions due to cache invalidation. For immediate effect in the current region, follow the update with a verification request. - operationId: updateKey - x-speakeasy-name-override: updateKey - security: - - rootKey: [] - requestBody: - content: - application/json: - schema: - "$ref": "#/components/schemas/V2KeysUpdateKeyRequestBody" - examples: - disableKey: - summary: Temporarily disable a key - value: - keyId: key_2cGKbMxRyIzhCxo1Idjz8q - enabled: false - updateMetadata: - summary: Update key metadata - value: - keyId: key_2cGKbMxRyIzhCxo1Idjz8q - meta: - plan: enterprise - features: - advancedAnalytics: true - customReports: true - updatedAt: "2023-07-15T12:30:45Z" - changeExpiration: - summary: Change expiration date - value: - keyId: key_2cGKbMxRyIzhCxo1Idjz8q - expires: 1735689600000 - updateLimits: - summary: Modify usage limits - value: - keyId: key_2cGKbMxRyIzhCxo1Idjz8q - credits: - remaining: 5000 - refill: - interval: monthly - amount: 5000 - refillDay: 1 - ratelimits: - - name: requests - limit: 200 - duration: 60000 - removeFeature: - summary: Remove a feature - value: - keyId: key_2cGKbMxRyIzhCxo1Idjz8q - expires: - meta: - required: true - responses: - "200": - content: - application/json: - schema: - "$ref": "#/components/schemas/V2KeysUpdateKeyResponseBody" - examples: - success: - summary: Successful update - value: - meta: - requestId: req_01H9TQPP77V5E48E9SH0BG0ZQY - data: {} - description: - Key successfully updated. Changes may take up to 30 seconds - to propagate to all regions due to cache invalidation. The response is - empty by design - use keys.getKey to retrieve the current state if needed. - "400": - description: Bad request - content: - application/json: - schema: - "$ref": "#/components/schemas/BadRequestErrorResponse" - "401": - description: Unauthorized - content: - application/json: - schema: - "$ref": "#/components/schemas/UnauthorizedErrorResponse" - "403": - description: Forbidden - content: - application/json: - schema: - "$ref": "#/components/schemas/ForbiddenErrorResponse" - "404": - description: Not found - content: - application/json: - schema: - "$ref": "#/components/schemas/NotFoundErrorResponse" - "500": - content: - application/json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - description: - Internal server error - Problem with key generation or database - operations - "/v2/keys.deleteKey": - post: - tags: - - keys - summary: Delete an API key - description: |- - Permanently deletes an API key, preventing it from being used for future verification attempts. This endpoint is used when a key is no longer needed, has been compromised, or needs to be revoked. - - When a key is deleted: - - Verification attempts will fail with `code=NOT_FOUND` - - The key no longer appears in API key listings - - Historical verification data is preserved for analytics - - This endpoint supports two deletion modes: - - Soft deletion (default): Marks the key as deleted but preserves its data - - Permanent deletion: Completely removes all traces of the key from the database - - Permanent deletion is useful for regulatory compliance (e.g., GDPR), resolving hash collisions during migrations, or when you need to reuse the same key string in the future. - operationId: deleteKey - x-speakeasy-name-override: deleteKey - security: - - rootKey: [] - requestBody: - content: - application/json: - schema: - "$ref": "#/components/schemas/V2KeysDeleteKeyRequestBody" - required: true - responses: - "200": - content: - application/json: - schema: - "$ref": "#/components/schemas/V2KeysDeleteKeyResponseBody" - description: - Successfully deleted key. The key will immediately fail verification - in the current region, but it may take up to 30 seconds for deletion to - propagate to all regions due to eventual consistency. After deletion, - the key will no longer appear in key listings, and verification attempts - will return `code=NOT_FOUND`. Soft-deleted keys (`permanent=false`) remain - in the database but are marked as deleted, while permanently deleted keys - are completely removed. - "400": - description: Bad request - content: - application/json: - schema: - "$ref": "#/components/schemas/BadRequestErrorResponse" - "401": - description: Unauthorized - content: - application/json: - schema: - "$ref": "#/components/schemas/UnauthorizedErrorResponse" - "403": - description: Forbidden - content: - application/json: - schema: - "$ref": "#/components/schemas/ForbiddenErrorResponse" - "404": - description: Not found - content: - application/json: - schema: - "$ref": "#/components/schemas/NotFoundErrorResponse" - "500": - content: - application/json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - description: - Internal server error - Problem with database operations or - cache invalidation - "/v2/keys.getKey": - post: - tags: - - keys - summary: Retrieve detailed information about an API key - description: |- - Retrieves comprehensive information about a specific API key by its ID. This endpoint provides access to all key properties, metadata, permissions, and usage limits. - - This is particularly useful for: - - Displaying key details in administrative interfaces - - Checking key configuration before making updates - - Retrieving metadata associated with a key - - Verifying permissions and role assignments - - Checking expiration dates and usage limits - - With appropriate permissions and if the key was created with the recoverable option, this endpoint can also return the complete plaintext key value, though this should be used with caution for security reasons. - operationId: getKey - x-speakeasy-name-override: getKey - security: - - rootKey: [] - requestBody: - content: - application/json: - schema: - "$ref": "#/components/schemas/V2KeysGetKeyRequestBody" - required: true - responses: - "200": - content: - application/json: - schema: - "$ref": "#/components/schemas/V2KeysGetKeyResponseBody" - description: Successfully retrieved key - "400": - description: Bad request - content: - application/json: - schema: - "$ref": "#/components/schemas/BadRequestErrorResponse" - "401": - description: Unauthorized - content: - application/json: - schema: - "$ref": "#/components/schemas/UnauthorizedErrorResponse" - "403": - description: Forbidden - content: - application/json: - schema: - "$ref": "#/components/schemas/ForbiddenErrorResponse" - "404": - description: Not found - content: - application/json: - schema: - "$ref": "#/components/schemas/NotFoundErrorResponse" - "500": - content: - application/json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - description: Internal server error - "/v2/keys.createKey": - post: - tags: - - keys - summary: Create a new API key with customizable security features - description: |- - Creates a new API key with configurable security properties, usage limits, and metadata. This endpoint generates a cryptographically secure key that you can distribute to your users for authenticating with your API. - - The key is only returned once in the response - Unkey stores only a secure hash of the key, not the key itself. You must provide this key directly to your end user; it cannot be retrieved later (unless created with `recoverable: true`, which is less secure). - - Keys are associated with a specific API (via `apiId`), which helps isolate environments (dev/staging/prod) and prevents keys from being used across different services. Keys can be further configured with: - - Optional prefixes for visual identification - - Expiration dates for temporary access - - Usage limits (credits) for consumption-based APIs - - Rate limits for abuse prevention - - Permissions and roles for granular access control - - Metadata for storing context with the key - - Best practices include using environment-specific API IDs, meaningful prefixes, appropriate byte length for security needs, and avoiding storing keys in your databases. - operationId: createKey - x-speakeasy-name-override: createKey - security: - - rootKey: [] - requestBody: - content: - application/json: - schema: - "$ref": "#/components/schemas/V2KeysCreateKeyRequestBody" - examples: - simple: - summary: Basic key creation - value: - apiId: api_1234567890abcdef - prefix: prod - withMeta: - summary: Key with user-specific metadata - value: - apiId: api_1234567890abcdef - prefix: prod - name: User API Key - externalId: user_12345 - meta: - plan: premium - features: - analytics: true - exports: true - teamId: team_abc123 - withLimits: - summary: Key with usage limits - value: - apiId: api_1234567890abcdef - prefix: prod - externalId: user_12345 - expires: 1735689600000 - credits: - remaining: 1000 - refill: - interval: monthly - amount: 1000 - refillDay: 1 - ratelimits: - - name: requests - limit: 100 - duration: 60000 - - name: heavy_operations - limit: 10 - duration: 3600000 - withPermissions: - summary: Key with access control - value: - apiId: api_1234567890abcdef - prefix: prod - externalId: user_12345 - permissions: - - documents.read - - documents.write - - profile.view - roles: - - editor - required: true - responses: - "200": - content: - application/json: - schema: - "$ref": "#/components/schemas/V2KeysCreateKeyResponseBody" - examples: - standard: - summary: Successfully created key - value: - meta: - requestId: req_01H9TQPP77V5E48E9SH0BG0ZQX - data: - keyId: key_01H9TQP8NP8JN3X8HWSKPW43JE - key: prod_f4cc2d765275c206b7d76ff0e92e583067c4e33603fb4055d7ba3031cd7ce36a - description: - "Successfully created a new API key. The response includes - both the keyId (for reference in your system) and the full key string. - IMPORTANT: This is the only time the complete key is available - it cannot - be retrieved later. You must securely provide this key to your end user - immediately." - "400": - description: Bad request - content: - application/json: - schema: - "$ref": "#/components/schemas/BadRequestErrorResponse" - "401": - description: Unauthorized - content: - application/json: - schema: - "$ref": "#/components/schemas/UnauthorizedErrorResponse" - "403": - description: Forbidden - content: - application/json: - schema: - "$ref": "#/components/schemas/ForbiddenErrorResponse" - "404": - description: Not found - content: - application/json: - schema: - "$ref": "#/components/schemas/NotFoundErrorResponse" - "500": - content: - application/json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - description: Internal server error - "/v2/ratelimit.limit": - post: - tags: - - ratelimit - summary: Apply rate limiting to any identifier - description: |- - Checks and enforces rate limits for any identifier in your application. This is the core rate limiting endpoint that provides flexible, namespace-based rate limiting for any entity in your system. - - Unlike API key-based rate limiting, this endpoint can be used to limit any identifiable entity: - - Anonymous users by IP address - - Authenticated users by user ID - - Organizations by organization ID - - API clients by client ID - - Specific actions or resources by custom identifiers - - The endpoint is designed for high-performance rate limiting with predictable behavior. It returns information about limit status, remaining capacity, and reset times that can be communicated to consumers of your API or service. - - Features: - - Namespace-based organization for different limit types - - Custom costs for variable-weight operations - - Flexible time windows (1 second to 24 hours) - - Override support for custom limits for specific identifiers - - Consistent sliding window implementation - - Implementation tips: - - Always check the 'success' field to determine if the request should proceed - - Use the 'reset' time to implement intelligent client-side retry logic - - Leverage the 'remaining' count to display usage information to users - - Store the namespaceId after first use to avoid name lookups in high-volume scenarios - - Consider using smaller durations for security-critical operations - operationId: ratelimit.limit - x-speakeasy-name-override: limit - security: - - rootKey: [] - requestBody: - content: - application/json: - schema: - "$ref": "#/components/schemas/V2RatelimitLimitRequestBody" - examples: - basic: - summary: Basic rate limit check - value: - namespace: api.requests - identifier: user_abc123 - limit: 100 - duration: 60000 - ipLimit: - summary: IP-based rate limiting - value: - namespace: auth.login - identifier: 203.0.113.42 - limit: 5 - duration: 60000 - weightedCost: - summary: Operation with variable cost - value: - namespace: api.heavy_operations - identifier: user_def456 - limit: 50 - duration: 3600000 - cost: 5 - usingNamespaceId: - summary: Using namespace ID instead of name - value: - namespace: ns_1234567890abcdef - identifier: user_xyz789 - limit: 200 - duration: 60000 - required: true - responses: - "200": - content: - application/json: - schema: - "$ref": "#/components/schemas/V2RatelimitLimitResponseBody" - examples: - allowed: - summary: Request allowed - value: - meta: - requestId: req_01H9TQPP77V5E48E9SH0BG0ZQX - data: - limit: 100 - remaining: 99 - reset: 1714582980000 - success: true - limitReached: - summary: Rate limit exceeded - value: - meta: - requestId: req_01H9TQPP77V5E48E9SH0BG0ZQY - data: - limit: 100 - remaining: 0 - reset: 1714582980000 - success: false - withOverride: - summary: With custom override applied - value: - meta: - requestId: req_01H9TQPP77V5E48E9SH0BG0ZQZ - data: - limit: 1000 - remaining: 995 - reset: 1714582980000 - success: true - overrideId: ovr_2cGKbMxRyIzhCxo1Idjz8q - description: - Rate limit check completed. Even when the rate limit is exceeded, - this endpoint returns HTTP 200 OK - you must check the 'success' field - in the response to determine if the request is allowed. When success=false, - the client should be prevented from proceeding with their request. - "400": - description: Bad request - content: - application/json: - schema: - "$ref": "#/components/schemas/BadRequestErrorResponse" - "401": - description: Unauthorized - content: - application/json: - schema: - "$ref": "#/components/schemas/UnauthorizedErrorResponse" - "403": - description: Forbidden - content: - application/json: - schema: - "$ref": "#/components/schemas/ForbiddenErrorResponse" - "404": - description: Not Found - content: - application/json: - schema: - "$ref": "#/components/schemas/NotFoundErrorResponse" - "500": - content: - application/json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - description: - Internal server error - An unexpected error occurred while - processing the request. This could be due to database connectivity issues, - validation errors not caught by the schema, or internal service problems. - Check the error details and requestId for troubleshooting. - "/v2/permissions.listRoles": - post: - tags: - - permissions - summary: List all roles in a workspace - description: |- - Retrieves a paginated list of all roles defined in the workspace, including their assigned permissions. This endpoint is essential for role-based access control (RBAC) administration. - - Roles are collections of permissions that can be assigned to API keys, providing a convenient way to manage access control at scale. This endpoint allows you to: - - View all available roles for assignment - - Inspect which permissions are granted by each role - - Build role management interfaces - - Audit your RBAC configuration - - Results are paginated and sorted alphabetically by role name. Each role includes its complete set of assigned permissions. - operationId: listRoles - x-speakeasy-name-override: ListRoles - security: - - rootKey: [] - requestBody: - content: - application/json: - schema: - "$ref": "#/components/schemas/V2PermissionsListRolesRequestBody" - required: true - responses: - "200": - content: - application/json: - schema: - "$ref": "#/components/schemas/V2PermissionsListRolesResponseBody" - description: Successfully listed roles - "400": - description: Bad Request - content: - application/json: - schema: - "$ref": "#/components/schemas/BadRequestErrorResponse" - "401": - description: Unauthorized - content: - application/json: - schema: - "$ref": "#/components/schemas/UnauthorizedErrorResponse" - "403": - description: Forbidden - content: - application/json: - schema: - "$ref": "#/components/schemas/ForbiddenErrorResponse" - "404": - description: Not Found - content: - application/json: - schema: - "$ref": "#/components/schemas/NotFoundErrorResponse" - "500": - content: - application/json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - description: Internal Server Error - "/v2/permissions.createRole": - post: - tags: - - permissions - summary: Create a new role with permissions - description: |- - Creates a new role in the workspace with optional permission assignments. Roles are collections of permissions that can be assigned to API keys, providing a convenient way to manage access control at scale. - - Roles enable you to group related permissions together for easier management, assign consistent permission sets to multiple API keys, and implement role-based access control patterns that simplify permission management as your system grows. When creating a role, you can optionally assign existing permissions to it immediately, and the role becomes available for assignment to API keys as soon as it's created. - - Use roles to establish standardized access patterns in your application, making it easier to maintain consistent security policies across many API keys while reducing the complexity of individual permission management. - operationId: createRole - x-speakeasy-name-override: CreateRole - security: - - rootKey: [] - requestBody: - content: - application/json: - schema: - "$ref": "#/components/schemas/V2PermissionsCreateRoleRequestBody" - examples: - basic: - summary: Basic role creation - value: - name: content.editor - description: Can read and write content - withoutDescription: - summary: Role without description - value: - name: api.reader - required: true - responses: - "200": - content: - application/json: - schema: - "$ref": "#/components/schemas/V2PermissionsCreateRoleResponseBody" - description: Successfully created role - "400": - description: Bad Request - content: - application/json: - schema: - "$ref": "#/components/schemas/BadRequestErrorResponse" - "401": - description: Unauthorized - content: - application/json: - schema: - "$ref": "#/components/schemas/UnauthorizedErrorResponse" - "403": - description: Forbidden - content: - application/json: - schema: - "$ref": "#/components/schemas/ForbiddenErrorResponse" - "404": - description: Not Found - One or more specified permissions do not exist - content: - application/json: - schema: - "$ref": "#/components/schemas/NotFoundErrorResponse" - "409": - description: Conflict - A role with this name already exists - content: - application/json: - schema: - "$ref": "#/components/schemas/ConflictErrorResponse" - "500": - content: - application/json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - description: Internal Server Error - "/v2/ratelimit.setOverride": - post: - tags: - - ratelimit - summary: Create or update a custom rate limit for specific identifiers - description: |- - Creates a new or updates an existing rate limit override for specific identifiers within a namespace. - - Overrides allow you to implement special rate limiting rules that differ from the default limits. This is essential for: - - - Creating tiered rate limits for different customer segments - - Implementing premium tiers with higher limits - - Applying stricter limits to suspicious or abusive users - - Setting emergency limits during system stress - - Creating customized rate limiting policies - - When an override exists, it completely replaces the default rate limit for matching identifiers. The override takes effect immediately and will be applied to the very next rate limit check that matches the identifier pattern. - - Wildcard patterns can be used to create powerful matching rules that apply to groups of identifiers without having to create individual overrides for each one. - operationId: ratelimit.setOverride - x-speakeasy-name-override: setOverride - security: - - rootKey: [] - requestBody: - content: - application/json: - schema: - "$ref": "#/components/schemas/V2RatelimitSetOverrideRequestBody" - examples: - premium: - summary: Higher limit for premium user - value: - namespaceName: api.requests - identifier: premium_user_123 - limit: 1000 - duration: 60000 - wildcardPattern: - summary: Pattern matching with wildcard - value: - namespaceId: ns_1234567890abcdef - identifier: premium_* - limit: 500 - duration: 60000 - blockAbusive: - summary: Block abusive user - value: - namespaceName: api.requests - identifier: abusive_user_456 - limit: 0 - duration: 3600000 - required: true - responses: - "200": - content: - application/json: - schema: - "$ref": "#/components/schemas/V2RatelimitSetOverrideResponseBody" - description: - Identity successfully deleted. The operation has permanently - removed the identity from your workspace. Any API keys previously associated - with this identity remain functional but no longer share resources through - this identity. The externalId can now be reused for a new identity if - needed. - "400": - description: Bad request - content: - application/json: - schema: - "$ref": "#/components/schemas/BadRequestErrorResponse" - "401": - description: Unauthorized - content: - application/json: - schema: - "$ref": "#/components/schemas/UnauthorizedErrorResponse" - "403": - description: Forbidden - content: - application/json: - schema: - "$ref": "#/components/schemas/ForbiddenErrorResponse" - "404": - description: Not Found - content: - application/json: - schema: - "$ref": "#/components/schemas/NotFoundErrorResponse" - "500": - content: - application/json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - description: Error - "/v2/ratelimit.getOverride": - post: - tags: - - ratelimit - summary: Retrieve a specific rate limit override - description: |- - Retrieves the details of a specific rate limit override by its identifier pattern. Returns the exact configuration including the identifier pattern, custom limit value, duration window, and namespace association. - - Use this endpoint to inspect override configurations before making modifications, audit your rate limiting policies, or debug rate limiting behavior. The identifier must match exactly as it was specified when creating the override, including any wildcard patterns. - - This endpoint is essential for understanding why certain identifiers receive different rate limits than the default. When troubleshooting rate limiting issues, this endpoint helps verify that overrides are configured correctly and matching the expected identifiers. - operationId: ratelimit.getOverride - x-speakeasy-name-override: getOverride - security: - - rootKey: [] - requestBody: - content: - application/json: - schema: - "$ref": "#/components/schemas/V2RatelimitGetOverrideRequestBody" - examples: - byNameAndId: - summary: Get specific override - value: - namespaceName: api.requests - identifier: premium_user_123 - wildcardPattern: - summary: Get wildcard pattern override - value: - namespaceId: ns_1234567890abcdef - identifier: premium_* - required: true - responses: - "200": - content: - application/json: - schema: - "$ref": "#/components/schemas/V2RatelimitGetOverrideResponseBody" - description: OK - "400": - description: Bad request - content: - application/json: - schema: - "$ref": "#/components/schemas/BadRequestErrorResponse" - "401": - description: Unauthorized - content: - application/json: - schema: - "$ref": "#/components/schemas/UnauthorizedErrorResponse" - "403": - description: Forbidden - content: - application/json: - schema: - "$ref": "#/components/schemas/ForbiddenErrorResponse" - "404": - description: Not Found - content: - application/json: - schema: - "$ref": "#/components/schemas/NotFoundErrorResponse" - "500": - content: - application/json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - description: Error - "/v2/ratelimit.listOverrides": - post: - tags: - - ratelimit - summary: List all rate limit overrides for a namespace - description: |- - Retrieves a paginated list of all rate limit overrides defined for a specific namespace. Returns detailed information about each override including identifier patterns, limits, and durations. - - Use this endpoint to audit your rate limiting policies, build administrative dashboards, or manage and clean up override configurations. For namespaces with many overrides, results are paginated and can be retrieved in chunks using the cursor parameter. - - This endpoint is particularly useful when you need to review all custom rate limit rules for a namespace, find patterns in your override configurations, or implement management interfaces for rate limiting policies. - operationId: ratelimit.listOverrides - x-speakeasy-name-override: listOverrides - security: - - rootKey: [] - requestBody: - content: - application/json: - schema: - "$ref": "#/components/schemas/V2RatelimitListOverridesRequestBody" - examples: - byNamespaceName: - summary: List all overrides by namespace name - value: - namespaceName: api.requests - limit: 20 - byNamespaceId: - summary: List overrides by namespace ID - value: - namespaceId: ns_1234567890abcdef - limit: 50 - pagination: - summary: Fetch next page of results - value: - namespaceId: ns_1234567890abcdef - cursor: cursor_eyJsYXN0SWQiOiJvdnJfM2RITGNOeVN6SnppRHlwMkpla2E5ciJ9 - required: true - responses: - "200": - content: - application/json: - schema: - "$ref": "#/components/schemas/V2RatelimitListOverridesResponseBody" - description: OK - "400": - description: Bad request - content: - application/json: - schema: - "$ref": "#/components/schemas/BadRequestErrorResponse" - "401": - description: Unauthorized - content: - application/json: - schema: - "$ref": "#/components/schemas/UnauthorizedErrorResponse" - "403": - description: Forbidden - content: - application/json: - schema: - "$ref": "#/components/schemas/ForbiddenErrorResponse" - "404": - description: Not Found - content: - application/json: - schema: - "$ref": "#/components/schemas/NotFoundErrorResponse" - "500": - content: - application/json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - description: Error - "/v2/ratelimit.deleteOverride": - post: - tags: - - ratelimit - summary: Delete a rate limit override - description: |- - Permanently removes a rate limit override from a namespace. Once deleted, the affected identifiers immediately revert to using the default rate limits for that namespace. - - Use this endpoint to remove temporary overrides that are no longer needed, reset entities back to standard rate limits, or clean up outdated rate limiting rules. The deletion operation is immediate and permanent - once an override is deleted, it cannot be recovered and must be recreated using the setOverride endpoint if needed again. - operationId: ratelimit.deleteOverride - x-speakeasy-name-override: deleteOverride - security: - - rootKey: [] - requestBody: - content: - application/json: - schema: - "$ref": "#/components/schemas/V2RatelimitDeleteOverrideRequestBody" - examples: - specificOverride: - summary: Delete specific identifier override - value: - namespaceName: api.requests - identifier: premium_user_123 - wildcardPattern: - summary: Delete wildcard pattern override - value: - namespaceId: ns_1234567890abcdef - identifier: premium_* - required: true - responses: - "200": - content: - application/json: - schema: - "$ref": "#/components/schemas/V2RatelimitDeleteOverrideResponseBody" - description: OK - "400": - description: Bad request - content: - application/json: - schema: - "$ref": "#/components/schemas/BadRequestErrorResponse" - "401": - description: Unauthorized - content: - application/json: - schema: - "$ref": "#/components/schemas/UnauthorizedErrorResponse" - "403": - description: Forbidden - content: - application/json: - schema: - "$ref": "#/components/schemas/ForbiddenErrorResponse" - "404": - description: Not Found - content: - application/json: - schema: - "$ref": "#/components/schemas/NotFoundErrorResponse" - "500": - content: - application/json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - description: Error - "/v2/identities.createIdentity": - post: - tags: - - identities - summary: Create a new identity for resource sharing across keys - description: |- - Creates a new identity that can be associated with multiple API keys to enable resource sharing. - - Identities are a powerful concept in Unkey that allow multiple API keys to share resources like rate limits and to be associated with the same entity (user, organization, etc.). This is essential for: - - - Implementing consistent rate limiting across multiple API keys - - Associating multiple keys with the same user or organization - - Sharing metadata across all keys for an entity - - Enabling user-based analytics across multiple keys - - Simplifying key management for multi-device or multi-service users - - When you create keys with the same externalId, they'll share rate limits and appear grouped in analytics. This enables scenarios like letting users generate multiple API keys for different devices while still treating them as a single entity for usage limits. - - The identity concept creates a separation between your user entities and their authentication credentials (API keys), similar to how users can have multiple passwords or sessions. - operationId: identities.createIdentity - x-speakeasy-name-override: createIdentity - security: - - rootKey: [] - requestBody: - content: - application/json: - schema: - "$ref": "#/components/schemas/V2IdentitiesCreateIdentityRequestBody" - examples: - basic: - summary: Basic identity creation - value: - externalId: user_abc123 - withMetadata: - summary: Identity with user metadata - value: - externalId: user_abc123 - meta: - name: Alice Smith - email: alice@example.com - plan: premium - accountCreated: "2023-01-15T08:30:00Z" - region: eu-west - withRatelimits: - summary: Identity with shared rate limits - value: - externalId: org_xyz456 - meta: - name: Acme Corporation - planTier: enterprise - ratelimits: - - name: requests - limit: 10000 - duration: 3600000 - autoApply: true - - name: heavy_compute - limit: 100 - duration: 86400000 - autoApply: false - required: true - responses: - "200": - content: - application/json: - schema: - "$ref": "#/components/schemas/V2IdentitiesCreateIdentityResponseBody" - examples: - success: - summary: Successfully created identity - value: - meta: - requestId: req_01H9TQPP77V5E48E9SH0BG0ZQX - description: OK - "400": - description: Bad request - content: - application/problem+json: - schema: - "$ref": "#/components/schemas/BadRequestErrorResponse" - "401": - description: Unauthorized - content: - application/problem+json: - schema: - "$ref": "#/components/schemas/UnauthorizedErrorResponse" - "403": - description: Forbidden - content: - application/problem+json: - schema: - "$ref": "#/components/schemas/ForbiddenErrorResponse" - "409": - description: - Conflict - An identity with this externalId already exists. - Each externalId must be unique within your workspace. This error occurs - when you attempt to create an identity with an externalId that's already - in use. To update an existing identity, use the updateIdentity endpoint - instead. If you're implementing automatic identity creation, you may want - to handle this error by falling back to getting the existing identity. - content: - application/problem+json: - schema: - "$ref": "#/components/schemas/ConflictErrorResponse" - "500": - content: - application/problem+json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - description: Error - "/v2/identities.getIdentity": - post: - tags: - - identities - summary: Retrieve identity information - description: |- - Retrieves detailed information about a specific identity by its external ID. - - Identities in Unkey represent entities in your system (users, organizations, etc.) that can be associated with multiple API keys. This endpoint provides access to: - - Identity metadata that was stored during creation - - Rate limiting configurations that apply across all keys for this identity - - This endpoint is useful for: - - Checking if an identity exists - - Retrieving metadata associated with an identity - - Viewing rate limit configurations - - You must provide the externalId parameter to identify which identity to retrieve. - operationId: identities.getIdentity - x-speakeasy-name-override: getIdentity - security: - - rootKey: [] - requestBody: - content: - application/json: - schema: - "$ref": "#/components/schemas/V2IdentitiesGetIdentityRequestBody" - examples: - byExternalId: - summary: Retrieve by external ID - value: - externalId: user_abc123 - required: true - responses: - "200": - description: Successfully retrieved the identity information - content: - application/json: - schema: - "$ref": "#/components/schemas/V2IdentitiesGetIdentityResponseBody" - examples: - standard: - summary: Identity with metadata and rate limits - value: - meta: - requestId: req_01H9TQPP77V5E48E9SH0BG0ZQX - data: - id: id_1234567890abcdef - externalId: user_abc123 - meta: - name: Alice Smith - email: alice@example.com - plan: premium - companyId: company_xyz - ratelimits: - - name: api_requests - limit: 1000 - duration: 60000 - - name: heavy_operations - limit: 100 - duration: 3600000 - "400": - description: Bad request - Missing the externalId, or other - validation issues - content: - application/json: - schema: - "$ref": "#/components/schemas/BadRequestErrorResponse" - "401": - description: Unauthorized - content: - application/json: - schema: - "$ref": "#/components/schemas/UnauthorizedErrorResponse" - "403": - description: Forbidden - Insufficient permissions to access this identity - content: - application/json: - schema: - "$ref": "#/components/schemas/ForbiddenErrorResponse" - "404": - description: Not found - The specified identity doesn't exist - content: - application/json: - schema: - "$ref": "#/components/schemas/NotFoundErrorResponse" - "500": - description: Internal server error - content: - application/json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - "/v2/identities.listIdentities": - post: - tags: - - identities - summary: List identities in your workspace - description: |- - Retrieves a paginated list of identities in your workspace. Identities represent entities in your system (users, organizations, etc.) that can be associated with multiple API keys for resource sharing. - - This endpoint allows you to: - - Browse all identities in your workspace - - Search for specific identities by environment - - View rate limits configured for each identity - - Implement identity management interfaces - - Results are paginated if there are more identities than the specified limit. Use the returned cursor to fetch subsequent pages. - operationId: identities.listIdentities - x-speakeasy-name-override: listIdentities - security: - - rootKey: [] - requestBody: - content: - application/json: - schema: - "$ref": "#/components/schemas/V2IdentitiesListIdentitiesRequestBody" - examples: - basic: - summary: Basic identity listing - value: - limit: 50 - withPagination: - summary: Fetch next page with cursor - value: - limit: 50 - cursor: cursor_eyJrZXkiOiJrZXlfMTIzNCJ9 - required: true - responses: - "200": - description: Successfully retrieved the list of identities - content: - application/json: - schema: - "$ref": "#/components/schemas/V2IdentitiesListIdentitiesResponseBody" - examples: - standard: - summary: Identity listing with pagination - value: - meta: - requestId: req_01H9TQPP77V5E48E9SH0BG0ZQX - data: - identities: - - id: id_01H9TQP8NP8JN3X8HWSKPW43JE - externalId: user_abc123 - ratelimits: - - name: api_requests - limit: 1000 - duration: 60000 - - name: heavy_operations - limit: 100 - duration: 3600000 - - id: id_02ZYR3Q9NP8JM4X8HWSKPW43JF - externalId: user_def456 - ratelimits: - - name: api_requests - limit: 500 - duration: 60000 - cursor: cursor_eyJsYXN0SWQiOiJpZF8wMlpZUjNROU5QOEpNNFg4SFdTS1BXNDNKRiJ9 - total: 247 - emptyList: - summary: Empty identity list - value: - meta: - requestId: req_01H9TQPP77V5E48E9SH0BG0ZQY - data: - identities: [] - total: 0 - "400": - description: Bad Request - Invalid parameters - content: - application/json: - schema: - "$ref": "#/components/schemas/BadRequestErrorResponse" - "401": - description: Unauthorized - Missing or invalid authentication - content: - application/json: - schema: - "$ref": "#/components/schemas/UnauthorizedErrorResponse" - "403": - description: Forbidden - Insufficient permissions - content: - application/json: - schema: - "$ref": "#/components/schemas/ForbiddenErrorResponse" - "500": - description: Internal Server Error - content: - application/json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - x-speakeasy-pagination: - type: cursor - inputs: - - name: cursor - in: parameters - type: cursor - outputs: - nextCursor: "$.data.cursor" - "/v2/identities.deleteIdentity": - post: - tags: - - identities - summary: Delete an existing identity - description: |- - Permanently removes an identity from your workspace. This operation cannot be undone. - - Deleting an identity has several important effects: - - All metadata associated with the identity is permanently removed - - Any rate limit history for this identity is cleared - - The identity's externalId becomes available for reuse - - Keys remain functional but lose their identity association - - Important notes: - - This operation does NOT delete or disable any API keys associated with this identity - - After deletion, keys previously linked to this identity will function independently - - Rate limits that were shared across keys via this identity will no longer be shared - - The externalId can be reused immediately after deletion - - Use this endpoint for compliance with data deletion requirements, cleaning up test data, or when an entity (user, organization) is permanently removed from your system. - operationId: v2.identities.deleteIdentity - x-speakeasy-name-override: deleteIdentity - security: - - rootKey: [] - requestBody: - content: - application/json: - schema: - "$ref": "#/components/schemas/V2IdentitiesDeleteIdentityRequestBody" - examples: - byExternalId: - summary: Delete by external ID - description: Deleting using your system's identifier - value: - externalId: user_abc123 - required: true - responses: - "200": - content: - application/json: - schema: - "$ref": "#/components/schemas/V2IdentitiesDeleteIdentityResponseBody" - examples: - success: - summary: Successful deletion - value: - meta: - requestId: req_01H9TQPP77V5E48E9SH0BG0ZQX - description: OK - "400": - description: Bad request - content: - application/problem+json: - schema: - "$ref": "#/components/schemas/BadRequestErrorResponse" - "401": - description: Unauthorized - content: - application/problem+json: - schema: - "$ref": "#/components/schemas/UnauthorizedErrorResponse" - "403": - description: Forbidden - content: - application/problem+json: - schema: - "$ref": "#/components/schemas/ForbiddenErrorResponse" - "404": - description: Not found - content: - application/problem+json: - schema: - "$ref": "#/components/schemas/NotFoundErrorResponse" - "500": - content: - application/problem+json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - description: Error - "/v2/permissions.createPermission": - post: - tags: - - permissions - summary: Create a new permission - description: |- - Creates a new permission in the workspace. Permissions are the fundamental building blocks of access control in Unkey's RBAC system, representing specific actions or capabilities that can be granted to API keys either directly or through roles. - - This endpoint allows you to: - - Define new permissions with descriptive names and URL-safe slugs - - Set up granular access control for your API keys - - Build the foundation for role-based access control - - Organize permissions with consistent naming patterns - - Best practices: - - Use hierarchical naming (e.g., 'resource.action' or 'resource.subresource.action') - - Choose descriptive but concise permission names - - Use URL-safe slugs for easy reference in APIs - operationId: createPermission - x-speakeasy-name-override: CreatePermission - security: - - rootKey: [] - requestBody: - content: - application/json: - schema: - "$ref": "#/components/schemas/V2PermissionsCreatePermissionRequestBody" - examples: - basic: - summary: Basic permission creation - value: - name: documents.read - slug: documents-read - description: Allows reading document resources - withoutDescription: - summary: Permission without description - value: - name: files.upload - slug: files-upload - complexPermission: - summary: Complex permission with detailed info - value: - name: admin.users.delete - slug: admin-users-delete - description: - Grants full administrative access to delete user accounts. - This is a high-privilege permission that should be used carefully. - required: true - responses: - "200": - content: - application/json: - schema: - "$ref": "#/components/schemas/V2PermissionsCreatePermissionResponseBody" - description: Successfully created permission - "400": - description: Bad Request - content: - application/json: - schema: - "$ref": "#/components/schemas/BadRequestErrorResponse" - "401": - description: Unauthorized - content: - application/json: - schema: - "$ref": "#/components/schemas/UnauthorizedErrorResponse" - "403": - description: Forbidden - content: - application/json: - schema: - "$ref": "#/components/schemas/ForbiddenErrorResponse" - "409": - description: Conflict - permission with that name already exists - content: - application/json: - schema: - "$ref": "#/components/schemas/ConflictErrorResponse" - "500": - content: - application/json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - description: Internal Server Error - "/v2/permissions.getPermission": - post: - tags: - - permissions - summary: Retrieve details about a specific permission - description: |- - Retrieves detailed information about a specific permission by its ID. This endpoint allows you to inspect a permission's properties, including its name, description, and workspace association. - - This is useful for: - - Verifying a permission exists before assigning it - - Retrieving permission details for display in administrative interfaces - - Checking permission configuration before making updates - - Building permission management tools - - Permissions are the fundamental building blocks of access control in Unkey's RBAC system, representing specific capabilities that can be granted to API keys either directly or through roles. - operationId: getPermission - x-speakeasy-name-override: GetPermission - security: - - rootKey: [] - requestBody: - content: - application/json: - schema: - "$ref": "#/components/schemas/V2PermissionsGetPermissionRequestBody" - required: true - responses: - "200": - content: - application/json: - schema: - "$ref": "#/components/schemas/V2PermissionsGetPermissionResponseBody" - description: Successfully retrieved permission - "400": - description: Bad Request - content: - application/json: - schema: - "$ref": "#/components/schemas/BadRequestErrorResponse" - "401": - description: Unauthorized - content: - application/json: - schema: - "$ref": "#/components/schemas/UnauthorizedErrorResponse" - "403": - description: Forbidden - content: - application/json: - schema: - "$ref": "#/components/schemas/ForbiddenErrorResponse" - "404": - description: Not Found - content: - application/json: - schema: - "$ref": "#/components/schemas/NotFoundErrorResponse" - "500": - content: - application/json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - description: Internal Server Error - "/v2/permissions.listPermissions": - post: - tags: - - permissions - summary: List all permissions in a workspace - description: |- - Retrieves a paginated list of all permissions defined in the workspace. This endpoint is essential for permission management and RBAC administration. - - Permissions are the fundamental building blocks of access control in Unkey, representing specific actions or capabilities that can be granted to API keys either directly or through roles. - - This endpoint allows you to: - - View all available permissions for assignment - - Build permission management interfaces - - Audit your RBAC configuration - - Identify existing permissions before creating new ones - - Results are paginated and typically sorted alphabetically by permission name. - operationId: listPermissions - x-speakeasy-name-override: ListPermissions - security: - - rootKey: [] - requestBody: - content: - application/json: - schema: - "$ref": "#/components/schemas/V2PermissionsListPermissionsRequestBody" - required: true - responses: - "200": - content: - application/json: - schema: - "$ref": "#/components/schemas/V2PermissionsListPermissionsResponseBody" - description: Successfully listed permissions - "400": - description: Bad Request - content: - application/json: - schema: - "$ref": "#/components/schemas/BadRequestErrorResponse" - "401": - description: Unauthorized - content: - application/json: - schema: - "$ref": "#/components/schemas/UnauthorizedErrorResponse" - "403": - description: Forbidden - content: - application/json: - schema: - "$ref": "#/components/schemas/ForbiddenErrorResponse" - "500": - content: - application/json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - description: Internal Server Error - "/v2/permissions.deletePermission": - post: - tags: - - permissions - summary: Delete a permission - description: |- - Permanently removes a permission from the workspace. This endpoint is used when a permission is no longer needed or when cleaning up unused permissions. - - When a permission is deleted: - - All direct assignments to API keys are removed - - The permission is removed from any roles it was assigned to - - The permission's name becomes available for reuse - - Important: This operation cannot be undone, and it immediately affects all API keys and roles that had this permission assigned. Consider carefully before deleting permissions that are actively in use. - operationId: deletePermission - x-speakeasy-name-override: DeletePermission - security: - - rootKey: [] - requestBody: - content: - application/json: - schema: - "$ref": "#/components/schemas/V2PermissionsDeletePermissionRequestBody" - examples: - basic: - summary: Delete a permission - value: - permissionId: perm_1234567890abcdef - required: true - responses: - "200": - content: - application/json: - schema: - "$ref": "#/components/schemas/V2PermissionsDeletePermissionResponseBody" - description: Successfully deleted permission - "400": - description: Bad Request - content: - application/json: - schema: - "$ref": "#/components/schemas/BadRequestErrorResponse" - "401": - description: Unauthorized - content: - application/json: - schema: - "$ref": "#/components/schemas/UnauthorizedErrorResponse" - "403": - description: Forbidden - content: - application/json: - schema: - "$ref": "#/components/schemas/ForbiddenErrorResponse" - "404": - description: Not Found - The specified permission does not exist - content: - application/json: - schema: - "$ref": "#/components/schemas/NotFoundErrorResponse" - "500": - content: - application/json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - description: Internal Server Error - "/v2/permissions.deleteRole": - post: - tags: - - permissions - summary: Delete a role - description: |- - Permanently removes a role from the workspace. This endpoint is used when a role is no longer needed or needs to be replaced with a different role structure. - - When a role is deleted: - - All keys that had this role assigned will lose the permissions granted by this role - - The role will no longer appear in role listings - - The role's name becomes available for reuse - - Important: This operation cannot be undone, and it immediately affects all API keys that had this role assigned. Consider carefully before deleting roles that are actively in use by many keys. - operationId: deleteRole - x-speakeasy-name-override: DeleteRole - security: - - rootKey: [] - requestBody: - content: - application/json: - schema: - "$ref": "#/components/schemas/V2PermissionsDeleteRoleRequestBody" - required: true - responses: - "200": - content: - application/json: - schema: - "$ref": "#/components/schemas/V2PermissionsDeleteRoleResponseBody" - description: Successfully deleted role - "400": - description: Bad Request - content: - application/json: - schema: - "$ref": "#/components/schemas/BadRequestErrorResponse" - "401": - description: Unauthorized - content: - application/json: - schema: - "$ref": "#/components/schemas/UnauthorizedErrorResponse" - "403": - description: Forbidden - content: - application/json: - schema: - "$ref": "#/components/schemas/ForbiddenErrorResponse" - "409": - description: Conflict - role with that name already exists - content: - application/json: - schema: - "$ref": "#/components/schemas/ConflictErrorResponse" - "500": - content: - application/json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - description: Internal Server Error - "/v2/permissions.getRole": - post: - tags: - - permissions - summary: Retrieve details about a specific role - description: |- - Retrieves detailed information about a specific role by its ID, including all permissions assigned to the role. This endpoint allows you to inspect a role's properties and understand what permissions it grants. - - This is useful for: - - Verifying a role exists before assigning it to API keys - - Retrieving role details for display in administrative interfaces - - Checking role configuration before making updates - - Building role management tools - - Understanding the complete permission set granted by a role - - Roles are collections of permissions that can be assigned to API keys, providing a convenient way to manage access control at scale in Unkey's RBAC system. - operationId: getRole - x-speakeasy-name-override: GetRole - security: - - rootKey: [] - requestBody: - content: - application/json: - schema: - "$ref": "#/components/schemas/V2PermissionsGetRoleRequestBody" - required: true - responses: - "200": - content: - application/json: - schema: - "$ref": "#/components/schemas/V2PermissionsGetRoleResponseBody" - description: Successfully retrieved role - "400": - description: Bad Request - content: - application/json: - schema: - "$ref": "#/components/schemas/BadRequestErrorResponse" - "401": - description: Unauthorized - content: - application/json: - schema: - "$ref": "#/components/schemas/UnauthorizedErrorResponse" - "403": - description: Forbidden - content: - application/json: - schema: - "$ref": "#/components/schemas/ForbiddenErrorResponse" - "404": - description: Not Found - content: - application/json: - schema: - "$ref": "#/components/schemas/NotFoundErrorResponse" - "500": - content: - application/json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - description: Internal Server Error - "/v2/apis.createApi": - post: - tags: - - apis - summary: Create a new API namespace for organizing keys - description: |- - Creates a new API namespace that serves as a container for related API keys. - - APIs in Unkey provide important functionality: - - They organize keys into logical groups for easier management - - They isolate keys between different environments (dev/staging/production) - - They enforce security boundaries between services or products - - They provide a foundation for permission scoping and access control - - Common use cases for creating separate APIs include: - - Separating development, staging, and production environments - - Isolating different services or microservices from each other - - Creating boundaries between different products or teams - - Implementing multi-tenant isolation for SaaS applications - - You'll use the resulting API ID when creating keys to associate them with this API namespace. During key verification, you must provide the same API ID to ensure keys can't be used across different environments or services. - operationId: createApi - x-speakeasy-name-override: createApi - security: - - rootKey: [] - requestBody: - content: - application/json: - schema: - "$ref": "#/components/schemas/V2ApisCreateApiRequestBody" - examples: - production: - summary: Production API - value: - name: payment-service-production - staging: - summary: Staging environment - value: - name: payment-service-staging - serviceSpecific: - summary: Service-specific API - value: - name: analytics-service - required: true - responses: - "200": - content: - application/json: - schema: - "$ref": "#/components/schemas/V2ApisCreateApiResponseBody" - examples: - success: - summary: Successfully created API - value: - meta: - requestId: req_01H9TQPP77V5E48E9SH0BG0ZQX - data: - apiId: api_2cGKbMxRjIzhCxo1IdjH3a - description: - Successfully created a new API namespace. The response includes - the unique API ID that you'll need for creating keys within this namespace. - This ID should be stored securely as it's required for all operations - related to this API. - "400": - description: Bad request - content: - application/json: - schema: - "$ref": "#/components/schemas/BadRequestErrorResponse" - "401": - description: Unauthorized - content: - application/json: - schema: - "$ref": "#/components/schemas/UnauthorizedErrorResponse" - "403": - description: Forbidden - content: - application/json: - schema: - "$ref": "#/components/schemas/ForbiddenErrorResponse" - "500": - content: - application/json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - description: Internal server error - "/v2/apis.deleteApi": - post: - tags: - - apis - summary: Delete an API and invalidate all its associated keys - description: |- - Permanently deletes an API namespace and invalidates all keys associated with it. - - WARNING: This is a destructive operation with significant consequences: - - All keys associated with this API will be invalidated immediately - - Verification attempts for these keys will fail with `code=NOT_FOUND` - - Historical analytics data for the API and its keys will be preserved - - This action cannot be undone - - Common reasons to delete an API include: - - Removing test or development environments no longer needed - - Retiring deprecated services or products - - Cleaning up unused resources - - Implementing security isolation after compromise - - Before deletion, ensure that: - - You have the correct API ID (check the environment/service) - - You have migrated any needed keys to a new API - - You have updated all client applications to use new keys - - You have backed up any important metadata or analytics - operationId: deleteApi - x-speakeasy-name-override: deleteApi - security: - - rootKey: [] - requestBody: - content: - application/json: - schema: - "$ref": "#/components/schemas/V2ApisDeleteApiRequestBody" - examples: - basic: - summary: Delete an API - value: - apiId: api_VNcuGfVjUkrVcWJmda0A - required: true - responses: - "200": - content: - application/json: - schema: - "$ref": "#/components/schemas/V2ApisDeleteApiResponseBody" - examples: - success: - summary: Successful deletion - value: - meta: - requestId: req_01H9TQPP77V5E48E9SH0BG0ZQX - description: - API successfully deleted. All keys associated with this API - have been invalidated and will no longer work for verification. The API - ID can no longer be used for any operations, and new keys cannot be created - with this API ID. This operation is immediate and cannot be undone. - "400": - description: Bad request - content: - application/json: - schema: - "$ref": "#/components/schemas/BadRequestErrorResponse" - "401": - description: Unauthorized - content: - application/json: - schema: - "$ref": "#/components/schemas/UnauthorizedErrorResponse" - "403": - description: Forbidden - content: - application/json: - schema: - "$ref": "#/components/schemas/ForbiddenErrorResponse" - "500": - content: - application/json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - description: Internal server error - "/v2/apis.getApi": - post: - tags: - - apis - summary: Retrieve information about an API namespace - description: |- - Retrieves detailed information about a specific API namespace. - - This endpoint is useful for: - - Verifying an API exists before attempting to use it - - Retrieving the name of an API when you only have its ID - - Checking if you have access to a particular API - - Confirming API details before performing operations on it - - The information returned is minimal by design, as APIs are primarily organizational containers. Most of the valuable data lies in the keys associated with an API, which can be retrieved using the apis.listKeys endpoint. - operationId: getApi - x-speakeasy-name-override: getApi - security: - - rootKey: [] - requestBody: - content: - application/json: - schema: - "$ref": "#/components/schemas/V2ApisGetApiRequestBody" - examples: - basic: - summary: Retrieve API by ID - value: - apiId: api_1234 - required: true - responses: - "200": - content: - application/json: - schema: - "$ref": "#/components/schemas/V2ApisGetApiResponseBody" - examples: - standard: - summary: API details response - value: - meta: - requestId: req_01H9TQPP77V5E48E9SH0BG0ZQX - data: - id: api_1234567890abcdef - name: payment-service-production - description: - Successfully retrieved API information. The response includes - the API's unique identifier and its name. This confirms the API exists - and provides its basic details. - "400": - description: Bad request - content: - application/json: - schema: - "$ref": "#/components/schemas/BadRequestErrorResponse" - "401": - description: Unauthorized - content: - application/json: - schema: - "$ref": "#/components/schemas/UnauthorizedErrorResponse" - "403": - description: Forbidden - content: - application/json: - schema: - "$ref": "#/components/schemas/ForbiddenErrorResponse" - "404": - description: Not found - content: - application/json: - schema: - "$ref": "#/components/schemas/NotFoundErrorResponse" - "500": - content: - application/json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - description: Internal server error - "/v2/apis.listKeys": - post: - tags: - - apis - summary: List all keys associated with an API namespace - description: |- - Retrieves a paginated list of all API keys associated with a specific API namespace. Returns detailed information about each key including metadata, permissions, and usage limits. - - Use this endpoint to build admin dashboards for API key management, audit active keys for security reviews, or find keys associated with specific users through the externalId filter. For large APIs with many keys, results are paginated and can be filtered to locate specific keys efficiently. - - For security reasons, full key values are never returned unless specifically requested with decrypt=true, and only for keys created with recoverable=true when the caller has sufficient permissions. - operationId: listKeys - x-speakeasy-name-override: listKeys - security: - - rootKey: [] - requestBody: - content: - application/json: - schema: - "$ref": "#/components/schemas/V2ApisListKeysRequestBody" - examples: - basicListing: - summary: List all keys for an API - value: - apiId: api_1234 - filteredListing: - summary: List keys for specific user - value: - apiId: api_1234 - externalId: user_5bf93ab218e - limit: 50 - paginatedRequest: - summary: Fetch next page of results - value: - apiId: api_1234 - cursor: cursor_eyJsYXN0S2V5SWQiOiJrZXlfMjNld3MiLCJsYXN0Q3JlYXRlZEF0IjoxNjcyNTI0MjM0MDAwfQ== - required: true - responses: - "200": - content: - application/json: - schema: - "$ref": "#/components/schemas/V2ApisListKeysResponseBody" - examples: - standardResponse: - summary: Successful key listing - value: - meta: - requestId: req_01H9TQPP77V5E48E9SH0BG0ZQX - data: - - keyId: key_1a2b3c4d5e6f - start: prod_abc - name: Production Admin Key - createdAt: 1671048264000 - permissions: - - admin.read - - admin.write - meta: - owner: alice@example.com - department: Engineering - - keyId: key_2b3c4d5e6f7g - start: prod_def - name: CI/CD Pipeline Key - createdAt: 1671135600000 - permissions: - - deploy.trigger - - logs.read - pagination: - cursor: cursor_eyJsYXN0S2V5SWQiOiJrZXlfMmIzYzRkNWU2ZjdnIiwibGFzdENyZWF0ZWRBdCI6MTY3MTEzNTYwMDAwMH0= - hasMore: true - description: - Successfully retrieved the list of keys for this API. The response - includes key details such as IDs, names, creation dates, and associated - metadata. For security, the actual key values are not included unless - specifically requested with decrypt=true. If there are more keys than - the requested limit, the pagination object will include a cursor for fetching - the next page and hasMore=true. - "400": - description: Bad Request - content: - application/json: - schema: - "$ref": "#/components/schemas/BadRequestErrorResponse" - "401": - description: Unauthorized - content: - application/json: - schema: - "$ref": "#/components/schemas/UnauthorizedErrorResponse" - "403": - description: Forbidden - content: - application/json: - schema: - "$ref": "#/components/schemas/ForbiddenErrorResponse" - "404": - description: Not Found - content: - application/json: - schema: - "$ref": "#/components/schemas/NotFoundErrorResponse" - "500": - content: - application/json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - description: Internal Server Error - "/v2/liveness": - get: - x-speakeasy-ignore: "true" - tags: - - liveness - security: [] - operationId: liveness - description: |- - Checks if the Unkey API service is functioning correctly and ready to handle requests. Returns a simple health status that load balancers, monitoring tools, and orchestration systems can use to verify service availability. - - This endpoint requires no authentication and has minimal processing overhead for fast health verification. The response indicates whether the service can process requests normally or if there are issues requiring attention. Monitor this endpoint to detect service degradation and implement automated failover or alerting when the service becomes unhealthy. - responses: - "200": - content: - application/json: - schema: - "$ref": "#/components/schemas/V2LivenessResponseBody" - examples: - healthy: - summary: Healthy service - value: - meta: - requestId: req_01H9TQPP77V5E48E9SH0BG0ZQX - data: - message: OK - description: - Service is healthy and ready to process requests. The response - includes a simple 'OK' message indicating normal operation. Monitoring - systems can use this as confirmation that the service is functioning properly. - "412": - content: - application/json: - schema: - "$ref": "#/components/schemas/PreconditionFailedErrorResponse" - examples: - degraded: - summary: Service in degraded state - value: - meta: - requestId: req_01H9TQPP77V5E48E9SH0BG0ZQX - error: - title: Precondition Failed - detail: - Service is in a degraded state. Some functionality may - be limited. - status: 412 - type: https://unkey.dev/errors/precondition-failed - description: - Service is operational but in a degraded state. Some functionality - may be limited or running with reduced performance. While the core service - is running, some dependent systems or features might be unavailable. This - status suggests caution when using the service. - "500": - content: - application/json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - examples: - unhealthy: - summary: Service unhealthy - value: - meta: - requestId: req_01H9TQPP77V5E48E9SH0BG0ZQY - error: - title: Internal Server Error - detail: - The service is currently experiencing issues and may - not be fully operational. - status: 500 - type: https://unkey.dev/errors/internal-server-error - description: - Service is unhealthy and unable to process requests properly. - This indicates a significant issue with the service that requires attention. - Clients should refrain from making other API calls as they are likely - to fail. Monitoring systems should trigger alerts when receiving this - response. - summary: Check service health and availability - "/v2/keys.verifyKey": - post: - tags: - - keys - security: - - rootKey: [] - summary: Verify an API key's validity and permissions - description: |- - This is the core endpoint for authenticating and authorizing API key usage in your application. It checks if a key is valid, hasn't expired, has sufficient remaining credits, isn't rate limited, and has the required permissions. - - When a key is verified, several checks occur: - - Key existence and format validation - - Expiration check - - Enabled/disabled status verification - - Credits deduction (if configured) - - Rate limit enforcement (if configured) - - Permission verification (if requested) - - This endpoint is designed for high-performance verification with global edge caching. Use it in your API authentication middleware to protect your endpoints. For high-volume applications, consider enabling async rate limiting for improved performance at the cost of slightly reduced accuracy. - - Unlike most other endpoints, this one does not require authentication with a root key. - operationId: verifyKey - x-speakeasy-name-override: verifyKey - requestBody: - content: - application/json: - schema: - "$ref": "#/components/schemas/V2KeysVerifyKeyRequestBody" - examples: - basic: - summary: Basic verification - value: - apiId: api_1234567890abcdef - key: my_prefix_1234567890abcdef - withPermissions: - summary: Verification with permission check - value: - apiId: api_1234567890abcdef - key: my_prefix_1234567890abcdef - permissions: - type: and - permissions: - - users.read - - users.edit - withRatelimits: - summary: Verification with custom rate limits - value: - apiId: api_1234567890abcdef - key: my_prefix_1234567890abcdef - ratelimits: - - name: requests - cost: 1 - - name: heavy_operations - cost: 5 - withAllOptions: - summary: Comprehensive verification - value: - apiId: api_1234567890abcdef - key: my_prefix_1234567890abcdef - permissions: admin.access - credits: - cost: 2 - ratelimits: - - name: api_calls - cost: 1 - tags: - - path=/api/resources - - method=POST - - ip=203.0.113.42 - required: true - responses: - "200": - content: - application/json: - schema: - "$ref": "#/components/schemas/V2KeysVerifyKeyResponseBody" - examples: - valid: - summary: Valid key with full details - value: - meta: - requestId: req_01H9TQPP77V5E48E9SH0BG0ZQX - data: - valid: true - code: VALID - keyId: key_01H9TQP8NP8JN3X8HWSKPW43JE - externalId: user_123456 - meta: - plan: premium - teamId: team_abc123 - ratelimits: - - name: requests - limit: 100 - remaining: 99 - reset: 1714582980000 - permissions: - - documents.read - - documents.write - roles: - - editor - invalid: - summary: Invalid key (usage exceeded) - value: - meta: - requestId: req_01H9TQPP77V5E48E9SH0BG0ZQX - data: - valid: false - code: USAGE_EXCEEDED - keyId: key_01H9TQP8NP8JN3X8HWSKPW43JE - ratelimited: - summary: Rate limited key - value: - meta: - requestId: req_01H9TQPP77V5E48E9SH0BG0ZQX - data: - valid: false - code: RATE_LIMITED - keyId: key_01H9TQP8NP8JN3X8HWSKPW43JE - ratelimits: - - name: requests - limit: 100 - remaining: 0 - reset: 1714582980000 - insufficientPermissions: - summary: Missing required permissions - value: - meta: - requestId: req_01H9TQPP77V5E48E9SH0BG0ZQX - data: - valid: false - code: INSUFFICIENT_PERMISSIONS - keyId: key_01H9TQP8NP8JN3X8HWSKPW43JE - permissions: - - documents.read - description: - Verification result. Note that even invalid keys return a 200 - status code - you must check the 'valid' field in the response to determine - if the key is valid. This design allows your application to handle different - failure reasons (expired, rate limited, etc.) differently while maintaining - a consistent API response structure. - "400": - description: Bad request - content: - application/json: - schema: - "$ref": "#/components/schemas/BadRequestErrorResponse" - examples: - permissionsQuerySyntaxError: - summary: Invalid permissions query syntax - value: - meta: - requestId: req_01H9TQPP77V5E48E9SH0BG0ZQX - error: - title: Bad Request - detail: "Syntax error in permission query: unexpected token 'AND' at position 15. Expected permission name or opening parenthesis." - status: 400 - type: "https://unkey.com/docs/api-reference/errors-v2/user/bad_request/permissions_query_syntax_error" - errors: - - location: "body.permissions" - message: "unexpected token 'AND' at position 15" - fix: "Check your query syntax. AND/OR operators must be between permissions, not at the start or end" - "401": - description: Unauthorized - content: - application/json: - schema: - "$ref": "#/components/schemas/UnauthorizedErrorResponse" - "403": - description: Forbidden - content: - application/json: - schema: - "$ref": "#/components/schemas/ForbiddenErrorResponse" - "404": - description: Not found - content: - application/json: - schema: - "$ref": "#/components/schemas/NotFoundErrorResponse" - "500": - content: - application/json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - description: Internal server error - "/v2/identities.updateIdentity": - post: - tags: - - identities - summary: Update an existing identity - description: |- - Updates an existing identity's metadata and rate limits with support for partial updates. You can modify the metadata to reflect changes in user status, subscription plans, or other entity properties, and adjust rate limits to match new usage tiers or requirements. - - This endpoint supports flexible identification using either the Unkey identity ID or your external ID, making it easy to integrate with existing user management systems. Fields not included in the request remain unchanged, allowing for targeted updates without affecting other identity properties. - - Changes to rate limits take effect immediately for new API key verifications, though existing sessions may retain old limits briefly due to caching. When updating metadata, consider that this information is returned during key verification, so avoid storing sensitive data that shouldn't be exposed to client applications. - - Common use cases include updating user plan information after subscription changes, adjusting rate limits for different service tiers, and maintaining current contact or organizational information associated with the identity. - operationId: v2.identities.updateIdentity - x-speakeasy-name-override: updateIdentity - security: - - rootKey: [] - requestBody: - content: - application/json: - schema: - "$ref": "#/components/schemas/V2IdentitiesUpdateIdentityRequestBody" - examples: - updateMetadata: - summary: Update identity metadata only - value: - externalId: user_123 - meta: - name: Alice Smith - email: alice.updated@example.com - plan: enterprise - required: true - responses: - "200": - content: - application/json: - schema: - "$ref": "#/components/schemas/V2IdentitiesUpdateIdentityResponseBody" - description: OK - "400": - description: Bad request - content: - application/problem+json: - schema: - "$ref": "#/components/schemas/BadRequestErrorResponse" - "401": - description: Unauthorized - content: - application/problem+json: - schema: - "$ref": "#/components/schemas/UnauthorizedErrorResponse" - "403": - description: Forbidden - content: - application/problem+json: - schema: - "$ref": "#/components/schemas/ForbiddenErrorResponse" - "404": - description: Not found - content: - application/problem+json: - schema: - "$ref": "#/components/schemas/NotFoundErrorResponse" - "500": - content: - application/problem+json: - schema: - "$ref": "#/components/schemas/InternalServerErrorResponse" - description: Error From 83e5a4425951e9818a7171566a0ec3f64e618104 Mon Sep 17 00:00:00 2001 From: chronark Date: Fri, 25 Jul 2025 09:21:49 +0200 Subject: [PATCH 8/9] fix: return proper errors --- go/apps/api/routes/v2_ratelimit_limit/handler.go | 8 ++++---- go/apps/api/routes/v2_ratelimit_list_overrides/handler.go | 5 ++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/go/apps/api/routes/v2_ratelimit_limit/handler.go b/go/apps/api/routes/v2_ratelimit_limit/handler.go index f17a3fb6f5..c0d6e242bd 100644 --- a/go/apps/api/routes/v2_ratelimit_limit/handler.go +++ b/go/apps/api/routes/v2_ratelimit_limit/handler.go @@ -20,7 +20,6 @@ import ( "github.com/unkeyed/unkey/go/pkg/fault" "github.com/unkeyed/unkey/go/pkg/match" "github.com/unkeyed/unkey/go/pkg/otel/logging" - "github.com/unkeyed/unkey/go/pkg/otel/tracing" "github.com/unkeyed/unkey/go/pkg/rbac" "github.com/unkeyed/unkey/go/pkg/zen" ) @@ -71,7 +70,6 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { // Use the namespace field directly - it can be either name or ID namespaceKey := req.Namespace - ctx, span := tracing.Start(ctx, "FindRatelimitNamespace") namespace, hit, err := h.RatelimitNamespaceCache.SWR(ctx, cache.ScopedKey{WorkspaceID: auth.AuthorizedWorkspaceID, Key: namespaceKey}, func(ctx context.Context) (db.FindRatelimitNamespace, error) { @@ -110,7 +108,6 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { return result, nil }, caches.DefaultFindFirstOp) - span.End() if err != nil { if db.IsNotFound(err) { @@ -120,7 +117,10 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { ) } - return err + return fault.Wrap(err, + fault.Code(codes.App.Internal.UnexpectedError.URN()), + fault.Public("An unexpected error occurred while fetching the namespace."), + ) } if hit == cache.Null { diff --git a/go/apps/api/routes/v2_ratelimit_list_overrides/handler.go b/go/apps/api/routes/v2_ratelimit_list_overrides/handler.go index 91381bf5c1..6c925bed16 100644 --- a/go/apps/api/routes/v2_ratelimit_list_overrides/handler.go +++ b/go/apps/api/routes/v2_ratelimit_list_overrides/handler.go @@ -59,7 +59,10 @@ func (h *Handler) Handle(ctx context.Context, s *zen.Session) error { fault.Internal("namespace not found"), fault.Public("This namespace does not exist."), ) } - return err + return fault.Wrap(err, + fault.Code(codes.App.Internal.UnexpectedError.URN()), + fault.Public("An unexpected error occurred while loading your namespace."), + ) } namespace := db.RatelimitNamespace{ From 223e8b2c4681aacbddecca48e9734831ac711dd5 Mon Sep 17 00:00:00 2001 From: chronark Date: Fri, 25 Jul 2025 09:44:42 +0200 Subject: [PATCH 9/9] fix: openapi ref casing --- go/apps/api/openapi/openapi-generated.yaml | 46 ++++--------------- .../spec/paths/chproxy/metrics/index.yaml | 6 +-- .../spec/paths/chproxy/ratelimits/index.yaml | 6 +-- .../paths/chproxy/verifications/index.yaml | 6 +-- 4 files changed, 18 insertions(+), 46 deletions(-) diff --git a/go/apps/api/openapi/openapi-generated.yaml b/go/apps/api/openapi/openapi-generated.yaml index af4e35ee46..5bb8e9146e 100644 --- a/go/apps/api/openapi/openapi-generated.yaml +++ b/go/apps/api/openapi/openapi-generated.yaml @@ -1,5 +1,5 @@ # Code generated by generate_bundle.go; DO NOT EDIT. -# Generated at: 2025-07-24T15:18:23Z +# Generated at: 2025-07-25T07:44:33Z # Source: openapi-split.yaml components: @@ -37,7 +37,7 @@ components: type: string description: Processing status example: "OK" - badRequestErrorResponse: + BadRequestErrorResponse: type: object required: - meta @@ -48,7 +48,7 @@ components: error: $ref: "#/components/schemas/BadRequestErrorDetails" description: Error response for invalid requests that cannot be processed due to client-side errors. This typically occurs when request parameters are missing, malformed, or fail validation rules. The response includes detailed information about the specific errors in the request, including the location of each error and suggestions for fixing it. When receiving this error, check the 'errors' array in the response for specific validation issues that need to be addressed before retrying. - internalServerErrorResponse: + InternalServerErrorResponse: type: object required: - meta @@ -121,17 +121,6 @@ components: data: $ref: "#/components/schemas/V2ApisCreateApiResponseData" additionalProperties: false - BadRequestErrorResponse: - type: object - required: - - meta - - error - properties: - meta: - $ref: "#/components/schemas/Meta" - error: - $ref: "#/components/schemas/BadRequestErrorDetails" - description: Error response for invalid requests that cannot be processed due to client-side errors. This typically occurs when request parameters are missing, malformed, or fail validation rules. The response includes detailed information about the specific errors in the request, including the location of each error and suggestions for fixing it. When receiving this error, check the 'errors' array in the response for specific validation issues that need to be addressed before retrying. UnauthorizedErrorResponse: type: object required: @@ -166,23 +155,6 @@ components: - Access to the requested resource is restricted based on workspace settings To resolve this error, ensure your root key has the necessary permissions or contact your workspace administrator. - InternalServerErrorResponse: - type: object - required: - - meta - - error - properties: - meta: - $ref: "#/components/schemas/Meta" - error: - $ref: "#/components/schemas/BaseError" - description: |- - Error response when an unexpected error occurs on the server. This indicates a problem with Unkey's systems rather than your request. - - When you encounter this error: - - The request ID in the response can help Unkey support investigate the issue - - The error is likely temporary and retrying may succeed - - If the error persists, contact Unkey support with the request ID V2ApisDeleteApiRequestBody: type: object required: @@ -3350,13 +3322,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/badRequestErrorResponse' + $ref: '#/components/schemas/BadRequestErrorResponse' description: Invalid request body or malformed events "529": content: application/json: schema: - $ref: '#/components/schemas/internalServerErrorResponse' + $ref: '#/components/schemas/InternalServerErrorResponse' description: Service overloaded, unable to process events security: [] summary: Internal ClickHouse proxy for API request metrics @@ -3388,13 +3360,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/badRequestErrorResponse' + $ref: '#/components/schemas/BadRequestErrorResponse' description: Invalid request body or malformed events "529": content: application/json: schema: - $ref: '#/components/schemas/internalServerErrorResponse' + $ref: '#/components/schemas/InternalServerErrorResponse' description: Service overloaded, unable to process events security: [] summary: Internal ClickHouse proxy for ratelimit events @@ -3426,13 +3398,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/badRequestErrorResponse' + $ref: '#/components/schemas/BadRequestErrorResponse' description: Invalid request body or malformed events "529": content: application/json: schema: - $ref: '#/components/schemas/internalServerErrorResponse' + $ref: '#/components/schemas/InternalServerErrorResponse' description: Service overloaded, unable to process events security: [] summary: Internal ClickHouse proxy for verification events diff --git a/go/apps/api/openapi/spec/paths/chproxy/metrics/index.yaml b/go/apps/api/openapi/spec/paths/chproxy/metrics/index.yaml index a2dfa57eb4..489ad29538 100644 --- a/go/apps/api/openapi/spec/paths/chproxy/metrics/index.yaml +++ b/go/apps/api/openapi/spec/paths/chproxy/metrics/index.yaml @@ -27,11 +27,11 @@ post: content: application/json: schema: - $ref: "../../../error/badRequestErrorResponse.yaml" + $ref: "../../../error/BadRequestErrorResponse.yaml" description: Invalid request body or malformed events "529": content: application/json: schema: - $ref: "../../../error/internalServerErrorResponse.yaml" - description: Service overloaded, unable to process events \ No newline at end of file + $ref: "../../../error/InternalServerErrorResponse.yaml" + description: Service overloaded, unable to process events diff --git a/go/apps/api/openapi/spec/paths/chproxy/ratelimits/index.yaml b/go/apps/api/openapi/spec/paths/chproxy/ratelimits/index.yaml index 28f40b8e20..b4a3599a27 100644 --- a/go/apps/api/openapi/spec/paths/chproxy/ratelimits/index.yaml +++ b/go/apps/api/openapi/spec/paths/chproxy/ratelimits/index.yaml @@ -27,11 +27,11 @@ post: content: application/json: schema: - $ref: "../../../error/badRequestErrorResponse.yaml" + $ref: "../../../error/BadRequestErrorResponse.yaml" description: Invalid request body or malformed events "529": content: application/json: schema: - $ref: "../../../error/internalServerErrorResponse.yaml" - description: Service overloaded, unable to process events \ No newline at end of file + $ref: "../../../error/InternalServerErrorResponse.yaml" + description: Service overloaded, unable to process events diff --git a/go/apps/api/openapi/spec/paths/chproxy/verifications/index.yaml b/go/apps/api/openapi/spec/paths/chproxy/verifications/index.yaml index 0bf5b66718..f118826d98 100644 --- a/go/apps/api/openapi/spec/paths/chproxy/verifications/index.yaml +++ b/go/apps/api/openapi/spec/paths/chproxy/verifications/index.yaml @@ -27,11 +27,11 @@ post: content: application/json: schema: - $ref: "../../../error/badRequestErrorResponse.yaml" + $ref: "../../../error/BadRequestErrorResponse.yaml" description: Invalid request body or malformed events "529": content: application/json: schema: - $ref: "../../../error/internalServerErrorResponse.yaml" - description: Service overloaded, unable to process events \ No newline at end of file + $ref: "../../../error/InternalServerErrorResponse.yaml" + description: Service overloaded, unable to process events