diff --git a/go/apps/api/openapi/gen.go b/go/apps/api/openapi/gen.go index 694dc4b1b6..5ccbdd8ddf 100644 --- a/go/apps/api/openapi/gen.go +++ b/go/apps/api/openapi/gen.go @@ -3,6 +3,10 @@ // Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.1 DO NOT EDIT. package openapi +const ( + RootKeyScopes = "rootKey.Scopes" +) + // BadRequestError defines model for BadRequestError. type BadRequestError struct { // Detail A human-readable explanation specific to this occurrence of the problem. @@ -60,6 +64,24 @@ type NotFoundError = BaseError // PreconditionFailedError defines model for PreconditionFailedError. type PreconditionFailedError = BaseError +// RatelimitOverride defines model for RatelimitOverride. +type RatelimitOverride struct { + // Duration The duration in milliseconds for the rate limit window. + Duration int64 `json:"duration"` + + // Identifier Identifier of your user, this can be their userId, an email, an ip or anything else. Wildcards ( * ) can be used to match multiple identifiers, More info can be found at https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules + Identifier string `json:"identifier"` + + // Limit The maximum number of requests allowed. + Limit int64 `json:"limit"` + + // NamespaceId The id of the namespace. + NamespaceId string `json:"namespaceId"` + + // OverrideId The id of the override. + OverrideId string `json:"overrideId"` +} + // UnauthorizedError defines model for UnauthorizedError. type UnauthorizedError = BaseError @@ -97,22 +119,7 @@ type V2RatelimitGetOverrideRequestBody struct { } // V2RatelimitGetOverrideResponseBody defines model for V2RatelimitGetOverrideResponseBody. -type V2RatelimitGetOverrideResponseBody struct { - // Duration The duration in milliseconds for the rate limit window. - Duration int64 `json:"duration"` - - // Identifier Identifier of your user, this can be their userId, an email, an ip or anything else. Wildcards ( * ) can be used to match multiple identifiers, More info can be found at https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules - Identifier string `json:"identifier"` - - // Limit The maximum number of requests allowed. - Limit int64 `json:"limit"` - - // NamespaceId The id of the namespace. - NamespaceId string `json:"namespaceId"` - - // OverrideId The id of the override. - OverrideId string `json:"overrideId"` -} +type V2RatelimitGetOverrideResponseBody = RatelimitOverride // V2RatelimitLimitRequestBody defines model for V2RatelimitLimitRequestBody. type V2RatelimitLimitRequestBody struct { @@ -150,6 +157,20 @@ type V2RatelimitLimitResponseBody struct { Success bool `json:"success"` } +// V2RatelimitListOverridesRequestBody List all overrides for a namespace +type V2RatelimitListOverridesRequestBody struct { + // NamespaceId The id of the namespace. Either namespaceId or namespaceName must be provided + NamespaceId *string `json:"namespaceId,omitempty"` + + // NamespaceName The name of the namespace. Either namespaceId or namespaceName must be provided + NamespaceName *string `json:"namespaceName,omitempty"` +} + +// V2RatelimitListOverridesResponseBody defines model for V2RatelimitListOverridesResponseBody. +type V2RatelimitListOverridesResponseBody struct { + Overrides []RatelimitOverride `json:"overrides"` +} + // V2RatelimitSetOverrideRequestBody Sets a new or overwrites an existing override. type V2RatelimitSetOverrideRequestBody struct { // Duration The duration in milliseconds for the rate limit window. @@ -186,14 +207,17 @@ type ValidationError struct { Message string `json:"message"` } -// V2RatelimitDeleteOverrideJSONRequestBody defines body for V2RatelimitDeleteOverride for application/json ContentType. -type V2RatelimitDeleteOverrideJSONRequestBody = V2RatelimitDeleteOverrideRequestBody +// RatelimitDeleteOverrideJSONRequestBody defines body for RatelimitDeleteOverride for application/json ContentType. +type RatelimitDeleteOverrideJSONRequestBody = V2RatelimitDeleteOverrideRequestBody -// V2RatelimitGetOverrideJSONRequestBody defines body for V2RatelimitGetOverride for application/json ContentType. -type V2RatelimitGetOverrideJSONRequestBody = V2RatelimitGetOverrideRequestBody +// RatelimitGetOverrideJSONRequestBody defines body for RatelimitGetOverride for application/json ContentType. +type RatelimitGetOverrideJSONRequestBody = V2RatelimitGetOverrideRequestBody // V1RatelimitLimitJSONRequestBody defines body for V1RatelimitLimit for application/json ContentType. type V1RatelimitLimitJSONRequestBody = V2RatelimitLimitRequestBody -// V2RatelimitSetOverrideJSONRequestBody defines body for V2RatelimitSetOverride for application/json ContentType. -type V2RatelimitSetOverrideJSONRequestBody = V2RatelimitSetOverrideRequestBody +// RatelimitListOverridesJSONRequestBody defines body for RatelimitListOverrides for application/json ContentType. +type RatelimitListOverridesJSONRequestBody = V2RatelimitListOverridesRequestBody + +// RatelimitSetOverrideJSONRequestBody defines body for RatelimitSetOverride for application/json ContentType. +type RatelimitSetOverrideJSONRequestBody = V2RatelimitSetOverrideRequestBody diff --git a/go/apps/api/openapi/openapi.json b/go/apps/api/openapi/openapi.json index 29637dccc5..b5f166e57d 100644 --- a/go/apps/api/openapi/openapi.json +++ b/go/apps/api/openapi/openapi.json @@ -208,42 +208,40 @@ "type": "object" }, "V2RatelimitGetOverrideResponseBody": { + "$ref": "#/components/schemas/RatelimitOverride" + }, + "V2RatelimitListOverridesRequestBody": { + "description": "List all overrides for a namespace", "additionalProperties": false, "properties": { "namespaceId": { - "description": "The id of the namespace.", - "type": "string", - "minLength": 1, - "maxLength": 255 - }, - "overrideId": { - "description": "The id of the override.", + "description": "The id of the namespace. Either namespaceId or namespaceName must be provided", "type": "string", "minLength": 1, "maxLength": 255 }, - "duration": { - "description": "The duration in milliseconds for the rate limit window.", - "format": "int64", - "type": "integer", - "minimum": 1000 - }, - "identifier": { - "description": "Identifier of your user, this can be their userId, an email, an ip or anything else. Wildcards ( * ) can be used to match multiple identifiers, More info can be found at https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules", + "namespaceName": { + "description": "The name of the namespace. Either namespaceId or namespaceName must be provided", "type": "string", "minLength": 1, "maxLength": 255 - }, - "limit": { - "description": "The maximum number of requests allowed.", - "format": "int64", - "type": "integer", - "minimum": 0 } }, - "required": ["namespaceId", "overrideId", "duration", "identifier", "limit"], "type": "object" }, + "V2RatelimitListOverridesResponseBody": { + "type": "object", + "additionalProperties": false, + "properties": { + "overrides": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RatelimitOverride" + } + } + }, + "required": ["overrides"] + }, "V2RatelimitLimitRequestBody": { "additionalProperties": false, "properties": { @@ -341,6 +339,43 @@ "V2RatelimitDeleteOverrideResponseBody": { "additionalProperties": false, "type": "object" + }, + "RatelimitOverride": { + "type": "object", + "additionalProperties": false, + "properties": { + "namespaceId": { + "description": "The id of the namespace.", + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "overrideId": { + "description": "The id of the override.", + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "duration": { + "description": "The duration in milliseconds for the rate limit window.", + "format": "int64", + "type": "integer", + "minimum": 1000 + }, + "identifier": { + "description": "Identifier of your user, this can be their userId, an email, an ip or anything else. Wildcards ( * ) can be used to match multiple identifiers, More info can be found at https://www.unkey.com/docs/ratelimiting/overrides#wildcard-rules", + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "limit": { + "description": "The maximum number of requests allowed.", + "format": "int64", + "type": "integer", + "minimum": 0 + } + }, + "required": ["namespaceId", "overrideId", "duration", "identifier", "limit"] } } }, @@ -595,6 +630,89 @@ } } }, + "/v2/ratelimit.listOverrides": { + "post": { + "tags": ["ratelimit"], + "operationId": "ratelimit.listOverrides", + "security": [ + { + "rootKey": [] + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/V2RatelimitListOverridesRequestBody" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/V2RatelimitListOverridesResponseBody" + } + } + }, + "description": "OK" + }, + "400": { + "description": "Bad request", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + } + } + }, + "500": { + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/InternalServerError" + } + } + }, + "description": "Error" + } + } + } + }, "/v2/ratelimit.deleteOverride": { "post": { "tags": ["ratelimit"], diff --git a/go/apps/api/routes/register.go b/go/apps/api/routes/register.go index a3fd9b57f1..ad385f7ebd 100644 --- a/go/apps/api/routes/register.go +++ b/go/apps/api/routes/register.go @@ -5,6 +5,7 @@ import ( v2RatelimitDeleteOverride "github.com/unkeyed/unkey/go/apps/api/routes/v2_ratelimit_delete_override" v2RatelimitGetOverride "github.com/unkeyed/unkey/go/apps/api/routes/v2_ratelimit_get_override" v2RatelimitLimit "github.com/unkeyed/unkey/go/apps/api/routes/v2_ratelimit_limit" + v2RatelimitListOverrides "github.com/unkeyed/unkey/go/apps/api/routes/v2_ratelimit_list_overrides" v2RatelimitSetOverride "github.com/unkeyed/unkey/go/apps/api/routes/v2_ratelimit_set_override" zen "github.com/unkeyed/unkey/go/pkg/zen" ) @@ -40,7 +41,6 @@ func Register(srv *zen.Server, svc *Services) { // --------------------------------------------------------------------------- // v2/ratelimit - // v2/ratelimit.limit srv.RegisterRoute( defaultMiddlewares, v2RatelimitLimit.New(v2RatelimitLimit.Services{ @@ -54,7 +54,6 @@ func Register(srv *zen.Server, svc *Services) { RatelimitOverrideMatchesCache: svc.Caches.RatelimitOverridesMatch, }), ) - // v2/ratelimit.setOverride srv.RegisterRoute( defaultMiddlewares, v2RatelimitSetOverride.New(v2RatelimitSetOverride.Services{ @@ -75,6 +74,16 @@ func Register(srv *zen.Server, svc *Services) { }), ) + srv.RegisterRoute( + defaultMiddlewares, + v2RatelimitListOverrides.New(v2RatelimitListOverrides.Services{ + Logger: svc.Logger, + DB: svc.Database, + Keys: svc.Keys, + Permissions: svc.Permissions, + }), + ) + srv.RegisterRoute( defaultMiddlewares, v2RatelimitDeleteOverride.New(v2RatelimitDeleteOverride.Services{ 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 new file mode 100644 index 0000000000..b6688ad848 --- /dev/null +++ b/go/apps/api/routes/v2_ratelimit_list_overrides/200_test.go @@ -0,0 +1,98 @@ +package handler_test + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/require" + handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_ratelimit_list_overrides" + "github.com/unkeyed/unkey/go/pkg/db" + "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/uid" +) + +func TestListOverridesSuccessfully(t *testing.T) { + ctx := context.Background() + h := testutil.NewHarness(t) + + // Create a namespace + namespaceID := uid.New("test_ns") + namespaceName := "test_namespace" + err := db.Query.InsertRatelimitNamespace(ctx, h.DB.RW(), db.InsertRatelimitNamespaceParams{ + ID: namespaceID, + WorkspaceID: h.Resources().UserWorkspace.ID, + Name: namespaceName, + CreatedAt: time.Now().UnixMilli(), + }) + require.NoError(t, err) + + // Create an override + identifier := "test_identifier" + limit := int32(10) + duration := int32(1000) + overrideID := uid.New(uid.RatelimitOverridePrefix) + + err = db.Query.InsertRatelimitOverride(ctx, h.DB.RW(), db.InsertRatelimitOverrideParams{ + ID: overrideID, + WorkspaceID: h.Resources().UserWorkspace.ID, + NamespaceID: namespaceID, + Identifier: identifier, + Limit: limit, + Duration: duration, + CreatedAt: time.Now().UnixMilli(), + }) + require.NoError(t, err) + + route := handler.New(handler.Services{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Permissions: h.Permissions, + }) + + h.Register(route) + + rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, fmt.Sprintf("ratelimit.%s.read_override", namespaceID)) + + headers := http.Header{ + "Content-Type": {"application/json"}, + "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, + } + + // Test getting by namespace name + t.Run("get by namespace name", func(t *testing.T) { + req := handler.Request{ + NamespaceName: &namespaceName, + } + + res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) + require.Equal(t, 200, res.Status, "expected 200, received: %v", res.Body) + require.NotNil(t, res.Body) + require.Len(t, res.Body.Overrides, 1) + require.Equal(t, overrideID, res.Body.Overrides[0].OverrideId) + require.Equal(t, namespaceID, res.Body.Overrides[0].NamespaceId) + require.Equal(t, identifier, res.Body.Overrides[0].Identifier) + require.Equal(t, int64(limit), res.Body.Overrides[0].Limit) + require.Equal(t, int64(duration), res.Body.Overrides[0].Duration) + }) + + // Test getting by namespace ID + t.Run("get by namespace ID", func(t *testing.T) { + req := handler.Request{ + NamespaceId: &namespaceID, + } + + res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) + require.Equal(t, 200, res.Status, "expected 200, received: %v", res.Body) + require.NotNil(t, res.Body) + require.Len(t, res.Body.Overrides, 1) + require.Equal(t, overrideID, res.Body.Overrides[0].OverrideId) + require.Equal(t, namespaceID, res.Body.Overrides[0].NamespaceId) + require.Equal(t, identifier, res.Body.Overrides[0].Identifier) + require.Equal(t, int64(limit), res.Body.Overrides[0].Limit) + require.Equal(t, int64(duration), res.Body.Overrides[0].Duration) + }) +} 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 new file mode 100644 index 0000000000..de969d589c --- /dev/null +++ b/go/apps/api/routes/v2_ratelimit_list_overrides/400_test.go @@ -0,0 +1,100 @@ +//nolint:exhaustruct +package handler_test + +import ( + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + "github.com/unkeyed/unkey/go/apps/api/openapi" + handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_ratelimit_list_overrides" + "github.com/unkeyed/unkey/go/pkg/testutil" +) + +func TestBadRequests(t *testing.T) { + h := testutil.NewHarness(t) + + rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID) + route := handler.New(handler.Services{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Permissions: h.Permissions, + }) + + h.Register(route) + + headers := http.Header{ + "Content-Type": {"application/json"}, + "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, + } + + t.Run("missing all required fields", func(t *testing.T) { + req := openapi.V2RatelimitListOverridesRequestBody{} + + res := testutil.CallRoute[handler.Request, openapi.BadRequestError](h, route, headers, req) + + require.Equal(t, 400, res.Status, "expected 400, sent: %+v, received: %s", req, res.RawBody) + require.NotNil(t, res.Body) + + require.Equal(t, "https://unkey.com/docs/errors/bad_request", res.Body.Type) + require.Equal(t, "You must provide either a namespace ID or name.", res.Body.Detail) + require.Equal(t, http.StatusBadRequest, res.Body.Status) + require.Equal(t, "Bad Request", res.Body.Title) + require.NotEmpty(t, res.Body.RequestId) + require.Nil(t, res.Body.Instance) + }) + + t.Run("neither namespace ID nor name provided", func(t *testing.T) { + req := openapi.V2RatelimitListOverridesRequestBody{ + NamespaceId: nil, + NamespaceName: nil, + } + + res := testutil.CallRoute[handler.Request, openapi.BadRequestError](h, route, headers, req) + + require.Equal(t, 400, res.Status, "expected 400, sent: %+v, received: %s", req, res.RawBody) + require.NotNil(t, res.Body) + + require.Equal(t, "https://unkey.com/docs/errors/bad_request", res.Body.Type) + require.Equal(t, "You must provide either a namespace ID or name.", res.Body.Detail) + require.Equal(t, http.StatusBadRequest, res.Body.Status) + require.Equal(t, "Bad Request", res.Body.Title) + require.NotEmpty(t, res.Body.RequestId) + require.Equal(t, len(res.Body.Errors), 0) + require.Nil(t, res.Body.Instance) + }) + + t.Run("missing authorization header", func(t *testing.T) { + headers := http.Header{ + "Content-Type": {"application/json"}, + // No Authorization header + } + + namespaceName := "test_namespace" + req := handler.Request{ + NamespaceName: &namespaceName, + } + + res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) + require.Equal(t, http.StatusBadRequest, res.Status) + require.NotNil(t, res.Body) + }) + + t.Run("malformed authorization header", func(t *testing.T) { + headers := http.Header{ + "Content-Type": {"application/json"}, + "Authorization": {"malformed_header"}, + } + + namespaceName := "test_namespace" + req := handler.Request{ + NamespaceName: &namespaceName, + } + + res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) + require.Equal(t, http.StatusBadRequest, res.Status) + require.NotNil(t, res.Body) + }) +} 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 new file mode 100644 index 0000000000..441a302d2e --- /dev/null +++ b/go/apps/api/routes/v2_ratelimit_list_overrides/401_test.go @@ -0,0 +1,40 @@ +package handler_test + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/require" + handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_ratelimit_list_overrides" + "github.com/unkeyed/unkey/go/pkg/testutil" +) + +func TestUnauthorizedAccess(t *testing.T) { + h := testutil.NewHarness(t) + + route := handler.New(handler.Services{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Permissions: h.Permissions, + }) + + h.Register(route) + + t.Run("invalid authorization token", func(t *testing.T) { + headers := http.Header{ + "Content-Type": {"application/json"}, + "Authorization": {"Bearer invalid_token"}, + } + + namespaceName := "test_namespace" + req := handler.Request{ + NamespaceName: &namespaceName, + } + + res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) + require.Equal(t, http.StatusUnauthorized, res.Status) + require.NotNil(t, res.Body) + }) + +} 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 new file mode 100644 index 0000000000..8ae908648a --- /dev/null +++ b/go/apps/api/routes/v2_ratelimit_list_overrides/403_test.go @@ -0,0 +1,75 @@ +package handler_test + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/unkeyed/unkey/go/apps/api/openapi" + handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_ratelimit_list_overrides" + "github.com/unkeyed/unkey/go/pkg/db" + "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/uid" +) + +func TestWorkspacePermissions(t *testing.T) { + ctx := context.Background() + h := testutil.NewHarness(t) + + // Create a namespace + namespaceID := uid.New(uid.RatelimitNamespacePrefix) + namespaceName := "test_namespace" + err := db.Query.InsertRatelimitNamespace(ctx, h.DB.RW(), db.InsertRatelimitNamespaceParams{ + ID: namespaceID, + WorkspaceID: h.Resources().UserWorkspace.ID, // Use the default workspace + Name: namespaceName, + CreatedAt: time.Now().UnixMilli(), + }) + require.NoError(t, err) + + // Create an override + identifier := "test_identifier" + overrideID := uid.New(uid.RatelimitOverridePrefix) + err = db.Query.InsertRatelimitOverride(ctx, h.DB.RW(), db.InsertRatelimitOverrideParams{ + ID: overrideID, + WorkspaceID: h.Resources().UserWorkspace.ID, // In default workspace + NamespaceID: namespaceID, + Identifier: identifier, + Limit: 10, + Duration: 1000, + CreatedAt: time.Now().UnixMilli(), + }) + require.NoError(t, err) + + route := handler.New(handler.Services{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Permissions: h.Permissions, + }) + + h.Register(route) + + // Create a key for a different workspace + differentWorkspaceID := "ws_different" + differentWorkspaceKey := h.CreateRootKey(differentWorkspaceID) + + headers := http.Header{ + "Content-Type": {"application/json"}, + "Authorization": {fmt.Sprintf("Bearer %s", differentWorkspaceKey)}, + } + + // Try to access the override with a key from a different workspace + req := handler.Request{ + NamespaceId: &namespaceID, + } + + res := testutil.CallRoute[handler.Request, openapi.BadRequestError](h, route, headers, req) + + // This should return a 404 Not Found (for security reasons we don't reveal if the namespace exists) + require.Equal(t, http.StatusNotFound, res.Status, "got: %s", res.RawBody) + require.NotNil(t, res.Body) +} 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 new file mode 100644 index 0000000000..26ce2dad3d --- /dev/null +++ b/go/apps/api/routes/v2_ratelimit_list_overrides/404_test.go @@ -0,0 +1,73 @@ +package handler_test + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/unkeyed/unkey/go/apps/api/openapi" + handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_ratelimit_list_overrides" + "github.com/unkeyed/unkey/go/pkg/db" + "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/uid" +) + +func TestOverrideNotFound(t *testing.T) { + ctx := context.Background() + h := testutil.NewHarness(t) + + // Create a namespace but no override + namespaceID := uid.New("test_ns") + namespaceName := "test_namespace" + err := db.Query.InsertRatelimitNamespace(ctx, h.DB.RW(), db.InsertRatelimitNamespaceParams{ + ID: namespaceID, + WorkspaceID: h.Resources().UserWorkspace.ID, + Name: namespaceName, + CreatedAt: time.Now().UnixMilli(), + }) + require.NoError(t, err) + + route := handler.New(handler.Services{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Permissions: h.Permissions, + }) + + h.Register(route) + rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "ratelimit.*.read_override") + + headers := http.Header{ + "Content-Type": {"application/json"}, + "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, + } + + // Test with non-existent namespace + t.Run("namespace not found", func(t *testing.T) { + nonExistentNamespaceId := "ns_nonexistent" + req := handler.Request{ + NamespaceId: &nonExistentNamespaceId, + } + + res := testutil.CallRoute[handler.Request, openapi.NotFoundError](h, route, headers, req) + require.Equal(t, http.StatusNotFound, res.Status) + require.NotNil(t, res.Body) + require.Equal(t, "https://unkey.com/docs/errors/not_found", res.Body.Type) + }) + + // Test with non-existent namespace name + t.Run("namespace name not found", func(t *testing.T) { + nonExistentNamespaceName := "nonexistent_namespace" + req := handler.Request{ + NamespaceName: &nonExistentNamespaceName, + } + + res := testutil.CallRoute[handler.Request, openapi.NotFoundError](h, route, headers, req) + require.Equal(t, http.StatusNotFound, res.Status) + require.NotNil(t, res.Body) + require.Equal(t, "https://unkey.com/docs/errors/not_found", res.Body.Type) + }) +} diff --git a/go/apps/api/routes/v2_ratelimit_list_overrides/handler.go b/go/apps/api/routes/v2_ratelimit_list_overrides/handler.go new file mode 100644 index 0000000000..2325552b37 --- /dev/null +++ b/go/apps/api/routes/v2_ratelimit_list_overrides/handler.go @@ -0,0 +1,138 @@ +package handler + +import ( + "context" + "database/sql" + "errors" + "net/http" + + "github.com/unkeyed/unkey/go/apps/api/openapi" + "github.com/unkeyed/unkey/go/internal/services/keys" + "github.com/unkeyed/unkey/go/internal/services/permissions" + "github.com/unkeyed/unkey/go/pkg/db" + "github.com/unkeyed/unkey/go/pkg/fault" + "github.com/unkeyed/unkey/go/pkg/otel/logging" + "github.com/unkeyed/unkey/go/pkg/rbac" + "github.com/unkeyed/unkey/go/pkg/zen" +) + +type Request = openapi.V2RatelimitListOverridesRequestBody +type Response = openapi.V2RatelimitListOverridesResponseBody + +type Services struct { + Logger logging.Logger + DB db.Database + Keys keys.KeyService + Permissions permissions.PermissionService +} + +func New(svc Services) zen.Route { + return zen.NewRoute("POST", "/v2/ratelimit.listOverrides", func(ctx context.Context, s *zen.Session) error { + auth, err := svc.Keys.VerifyRootKey(ctx, s) + if err != nil { + return err + } + + req := Request{} + err = s.BindBody(&req) + if err != nil { + return fault.Wrap(err, + fault.WithTag(fault.INTERNAL_SERVER_ERROR), + fault.WithDesc("invalid request body", "The request body is invalid."), + ) + } + + namespace, err := getNamespace(ctx, svc, auth.AuthorizedWorkspaceID, req) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return fault.Wrap(err, + fault.WithTag(fault.NOT_FOUND), + fault.WithDesc("namespace not found", "This namespace does not exist."), + ) + } + return err + } + if namespace.WorkspaceID != auth.AuthorizedWorkspaceID { + return fault.New("namespace not found", + fault.WithTag(fault.NOT_FOUND), + fault.WithDesc("wrong workspace, masking as 404", "This namespace does not exist."), + ) + } + + permissions, err := svc.Permissions.Check( + ctx, + auth.KeyID, + rbac.Or( + rbac.T(rbac.Tuple{ + ResourceType: rbac.Ratelimit, + ResourceID: namespace.ID, + Action: rbac.ReadOverride, + }), + rbac.T(rbac.Tuple{ + ResourceType: rbac.Ratelimit, + ResourceID: "*", + Action: rbac.ReadOverride, + }), + ), + ) + if err != nil { + return fault.Wrap(err, + fault.WithTag(fault.INTERNAL_SERVER_ERROR), + fault.WithDesc("unable to check permissions", "We're unable to check the permissions of your key."), + ) + } + + if !permissions.Valid { + return fault.New("insufficient permissions", + fault.WithTag(fault.INSUFFICIENT_PERMISSIONS), + fault.WithDesc(permissions.Message, permissions.Message), + ) + } + + overrides, err := db.Query.ListRatelimitOverrides(ctx, svc.DB.RO(), db.ListRatelimitOverridesParams{ + WorkspaceID: auth.AuthorizedWorkspaceID, + NamespaceID: namespace.ID, + }) + + if err != nil { + return err + } + + response := Response{ + Overrides: make([]openapi.RatelimitOverride, len(overrides)), + } + + for i, override := range overrides { + response.Overrides[i] = openapi.RatelimitOverride{ + OverrideId: override.ID, + Duration: int64(override.Duration), + Identifier: override.Identifier, + NamespaceId: override.NamespaceID, + Limit: int64(override.Limit), + } + } + + return s.JSON(http.StatusOK, response) + }) +} + +func getNamespace(ctx context.Context, svc Services, workspaceID string, req Request) (db.RatelimitNamespace, error) { + switch { + case req.NamespaceId != nil: + { + return db.Query.FindRatelimitNamespaceByID(ctx, svc.DB.RO(), *req.NamespaceId) + } + case req.NamespaceName != nil: + { + return db.Query.FindRatelimitNamespaceByName(ctx, svc.DB.RO(), db.FindRatelimitNamespaceByNameParams{ + WorkspaceID: workspaceID, + Name: *req.NamespaceName, + }) + } + } + + return db.RatelimitNamespace{}, fault.New("missing namespace id or name", + fault.WithTag(fault.BAD_REQUEST), + fault.WithDesc("missing namespace id or name", "You must provide either a namespace ID or name."), + ) +} diff --git a/go/gen/proto/ratelimit/v1/service.pb.go b/go/gen/proto/ratelimit/v1/service.pb.go index 94d970ce48..ab3f28faa5 100644 --- a/go/gen/proto/ratelimit/v1/service.pb.go +++ b/go/gen/proto/ratelimit/v1/service.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.5 +// protoc-gen-go v1.36.6 // protoc (unknown) // source: proto/ratelimit/v1/service.proto @@ -438,64 +438,37 @@ func (x *ReplayResponse) GetResponse() *RatelimitResponse { var File_proto_ratelimit_v1_service_proto protoreflect.FileDescriptor -var file_proto_ratelimit_v1_service_proto_rawDesc = string([]byte{ - 0x0a, 0x20, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x72, 0x61, 0x74, 0x65, 0x6c, 0x69, 0x6d, 0x69, - 0x74, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x12, 0x0c, 0x72, 0x61, 0x74, 0x65, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x2e, 0x76, 0x31, - 0x22, 0x78, 0x0a, 0x10, 0x52, 0x61, 0x74, 0x65, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, - 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, - 0x66, 0x69, 0x65, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x75, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x64, 0x75, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x73, 0x74, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x63, 0x6f, 0x73, 0x74, 0x22, 0x91, 0x01, 0x0a, 0x11, 0x52, - 0x61, 0x74, 0x65, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, - 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x72, 0x65, 0x6d, 0x61, 0x69, - 0x6e, 0x69, 0x6e, 0x67, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x65, 0x73, 0x65, 0x74, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x05, 0x72, 0x65, 0x73, 0x65, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, - 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, - 0x63, 0x65, 0x73, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x22, 0x70, - 0x0a, 0x06, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x71, 0x75, - 0x65, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x73, 0x65, 0x71, 0x75, - 0x65, 0x6e, 0x63, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, - 0x61, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, - 0x22, 0x75, 0x0a, 0x0d, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x38, 0x0a, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x72, 0x61, 0x74, 0x65, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x2e, 0x76, - 0x31, 0x2e, 0x52, 0x61, 0x74, 0x65, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x52, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, - 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x12, - 0x16, 0x0a, 0x06, 0x64, 0x65, 0x6e, 0x69, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x06, 0x64, 0x65, 0x6e, 0x69, 0x65, 0x64, 0x22, 0xaf, 0x01, 0x0a, 0x0e, 0x52, 0x65, 0x70, 0x6c, - 0x61, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x07, 0x63, 0x75, - 0x72, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x72, 0x61, - 0x74, 0x65, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x69, 0x6e, 0x64, 0x6f, - 0x77, 0x52, 0x07, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x12, 0x30, 0x0a, 0x08, 0x70, 0x72, - 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x72, - 0x61, 0x74, 0x65, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x69, 0x6e, 0x64, - 0x6f, 0x77, 0x52, 0x08, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x12, 0x3b, 0x0a, 0x08, - 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, - 0x2e, 0x72, 0x61, 0x74, 0x65, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x61, - 0x74, 0x65, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, - 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x59, 0x0a, 0x10, 0x52, 0x61, 0x74, - 0x65, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, - 0x06, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x79, 0x12, 0x1b, 0x2e, 0x72, 0x61, 0x74, 0x65, 0x6c, 0x69, - 0x6d, 0x69, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x79, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x72, 0x61, 0x74, 0x65, 0x6c, 0x69, 0x6d, 0x69, 0x74, - 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x42, 0x40, 0x5a, 0x3e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x75, 0x6e, 0x6b, 0x65, 0x79, 0x65, 0x64, 0x2f, 0x75, 0x6e, 0x6b, 0x65, 0x79, - 0x2f, 0x67, 0x6f, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x72, 0x61, - 0x74, 0x65, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x2f, 0x76, 0x31, 0x3b, 0x72, 0x61, 0x74, 0x65, 0x6c, - 0x69, 0x6d, 0x69, 0x74, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -}) +const file_proto_ratelimit_v1_service_proto_rawDesc = "" + + "\n" + + " proto/ratelimit/v1/service.proto\x12\fratelimit.v1\"x\n" + + "\x10RatelimitRequest\x12\x1e\n" + + "\n" + + "identifier\x18\x01 \x01(\tR\n" + + "identifier\x12\x14\n" + + "\x05limit\x18\x02 \x01(\x03R\x05limit\x12\x1a\n" + + "\bduration\x18\x03 \x01(\x03R\bduration\x12\x12\n" + + "\x04cost\x18\x04 \x01(\x03R\x04cost\"\x91\x01\n" + + "\x11RatelimitResponse\x12\x14\n" + + "\x05limit\x18\x01 \x01(\x03R\x05limit\x12\x1c\n" + + "\tremaining\x18\x02 \x01(\x03R\tremaining\x12\x14\n" + + "\x05reset\x18\x03 \x01(\x03R\x05reset\x12\x18\n" + + "\asuccess\x18\x04 \x01(\bR\asuccess\x12\x18\n" + + "\acurrent\x18\x05 \x01(\x03R\acurrent\"p\n" + + "\x06Window\x12\x1a\n" + + "\bsequence\x18\x01 \x01(\x03R\bsequence\x12\x1a\n" + + "\bduration\x18\x02 \x01(\x03R\bduration\x12\x18\n" + + "\acounter\x18\x03 \x01(\x03R\acounter\x12\x14\n" + + "\x05start\x18\x04 \x01(\x03R\x05start\"u\n" + + "\rReplayRequest\x128\n" + + "\arequest\x18\x01 \x01(\v2\x1e.ratelimit.v1.RatelimitRequestR\arequest\x12\x12\n" + + "\x04time\x18\x02 \x01(\x03R\x04time\x12\x16\n" + + "\x06denied\x18\x03 \x01(\bR\x06denied\"\xaf\x01\n" + + "\x0eReplayResponse\x12.\n" + + "\acurrent\x18\x01 \x01(\v2\x14.ratelimit.v1.WindowR\acurrent\x120\n" + + "\bprevious\x18\x02 \x01(\v2\x14.ratelimit.v1.WindowR\bprevious\x12;\n" + + "\bresponse\x18\x03 \x01(\v2\x1f.ratelimit.v1.RatelimitResponseR\bresponse2Y\n" + + "\x10RatelimitService\x12E\n" + + "\x06Replay\x12\x1b.ratelimit.v1.ReplayRequest\x1a\x1c.ratelimit.v1.ReplayResponse\"\x00B@Z>github.com/unkeyed/unkey/go/gen/proto/ratelimit/v1;ratelimitv1b\x06proto3" var ( file_proto_ratelimit_v1_service_proto_rawDescOnce sync.Once diff --git a/go/pkg/db/querier_generated.go b/go/pkg/db/querier_generated.go index 059fbb6361..7df7aac174 100644 --- a/go/pkg/db/querier_generated.go +++ b/go/pkg/db/querier_generated.go @@ -384,6 +384,13 @@ type Querier interface { // true // ) InsertWorkspace(ctx context.Context, db DBTX, arg InsertWorkspaceParams) error + //ListRatelimitOverrides + // + // SELECT id, workspace_id, namespace_id, identifier, `limit`, duration, async, sharding, created_at_m, updated_at_m, deleted_at_m FROM ratelimit_overrides + // WHERE + // workspace_id = ? + // AND namespace_id = ? + ListRatelimitOverrides(ctx context.Context, db DBTX, arg ListRatelimitOverridesParams) ([]RatelimitOverride, error) //ListWorkspaces // // SELECT diff --git a/go/pkg/db/queries/ratelimit_override_list_by_namespace_id.sql b/go/pkg/db/queries/ratelimit_override_list_by_namespace_id.sql new file mode 100644 index 0000000000..dd558d828c --- /dev/null +++ b/go/pkg/db/queries/ratelimit_override_list_by_namespace_id.sql @@ -0,0 +1,5 @@ +-- name: ListRatelimitOverrides :many +SELECT * FROM ratelimit_overrides +WHERE + workspace_id = sqlc.arg(workspace_id) + AND namespace_id = sqlc.arg(namespace_id); diff --git a/go/pkg/db/ratelimit_override_list_by_namespace_id.sql_generated.go b/go/pkg/db/ratelimit_override_list_by_namespace_id.sql_generated.go new file mode 100644 index 0000000000..626d919d39 --- /dev/null +++ b/go/pkg/db/ratelimit_override_list_by_namespace_id.sql_generated.go @@ -0,0 +1,63 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: ratelimit_override_list_by_namespace_id.sql + +package db + +import ( + "context" +) + +const listRatelimitOverrides = `-- name: ListRatelimitOverrides :many +SELECT id, workspace_id, namespace_id, identifier, ` + "`" + `limit` + "`" + `, duration, async, sharding, created_at_m, updated_at_m, deleted_at_m FROM ratelimit_overrides +WHERE + workspace_id = ? + AND namespace_id = ? +` + +type ListRatelimitOverridesParams struct { + WorkspaceID string `db:"workspace_id"` + NamespaceID string `db:"namespace_id"` +} + +// ListRatelimitOverrides +// +// SELECT id, workspace_id, namespace_id, identifier, `limit`, duration, async, sharding, created_at_m, updated_at_m, deleted_at_m FROM ratelimit_overrides +// WHERE +// workspace_id = ? +// AND namespace_id = ? +func (q *Queries) ListRatelimitOverrides(ctx context.Context, db DBTX, arg ListRatelimitOverridesParams) ([]RatelimitOverride, error) { + rows, err := db.QueryContext(ctx, listRatelimitOverrides, arg.WorkspaceID, arg.NamespaceID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []RatelimitOverride + for rows.Next() { + var i RatelimitOverride + if err := rows.Scan( + &i.ID, + &i.WorkspaceID, + &i.NamespaceID, + &i.Identifier, + &i.Limit, + &i.Duration, + &i.Async, + &i.Sharding, + &i.CreatedAtM, + &i.UpdatedAtM, + &i.DeletedAtM, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/packages/ratelimit/package.json b/packages/ratelimit/package.json index d64aba65f1..d3d17cb5ff 100644 --- a/packages/ratelimit/package.json +++ b/packages/ratelimit/package.json @@ -26,6 +26,6 @@ "typescript": "^5.5.3" }, "dependencies": { - "@unkey/api": "^2.0.0-alpha.1" + "@unkey/api": "2.0.0-alpha.5" } } diff --git a/packages/ratelimit/src/ratelimit.ts b/packages/ratelimit/src/ratelimit.ts index dcf5a86474..5e77b4a4ba 100644 --- a/packages/ratelimit/src/ratelimit.ts +++ b/packages/ratelimit/src/ratelimit.ts @@ -168,25 +168,13 @@ export class Ratelimit implements Ratelimiter { let timeoutId: any = null; try { const ps: Promise[] = [ - this.unkey.ratelimit - .limit({ - namespace: this.config.namespace, - identifier, - limit: opts?.limit?.limit ?? this.config.limit, - duration: ms(opts?.limit?.duration ?? this.config.duration), - cost: opts?.cost, - }) - .then(async (res) => { - if (res.statusCode !== 200 || !res.v2RatelimitLimitResponseBody) { - throw new Error( - `Ratelimit failed: [${res.statusCode} - ${res.rawResponse.headers.get( - "Unkey-Request-Id", - )}]: ${await res.rawResponse.text()}`, - ); - } - - return res.v2RatelimitLimitResponseBody; - }), + this.unkey.ratelimit.limit({ + namespace: this.config.namespace, + identifier, + limit: opts?.limit?.limit ?? this.config.limit, + duration: ms(opts?.limit?.duration ?? this.config.duration), + cost: opts?.cost, + }), ]; if (timeout) { ps.push( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 770ae1ad84..e27cf5923a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1617,8 +1617,8 @@ importers: packages/ratelimit: dependencies: '@unkey/api': - specifier: ^2.0.0-alpha.1 - version: 2.0.0-alpha.1(zod@3.24.2) + specifier: 2.0.0-alpha.5 + version: 2.0.0-alpha.5(zod@3.23.8) devDependencies: '@types/node': specifier: ^20.14.9 @@ -11790,12 +11790,17 @@ packages: '@unkey/rbac': 0.1.13 dev: false - /@unkey/api@2.0.0-alpha.1(zod@3.24.2): - resolution: {integrity: sha512-YDKPKdzPbw/bOKZXLYe2YKF7Efsw3NtUJxK0x3grAjlIjc33zikN0yIn2gG2klhvWYH5I/gdlCrQH3ymKZagCQ==} + /@unkey/api@2.0.0-alpha.5(zod@3.23.8): + resolution: {integrity: sha512-WUUUXLPyXSGpdFsSEP1+2/mxKpxyHkXOnoP+OHdvZ9NL4nagWTU3BBuDTNQfKOCfJaCgujU/+1xaWp267gkjmQ==} + hasBin: true peerDependencies: + '@modelcontextprotocol/sdk': ^1.5.0 zod: '>= 3' + peerDependenciesMeta: + '@modelcontextprotocol/sdk': + optional: true dependencies: - zod: 3.24.2 + zod: 3.23.8 dev: false /@unkey/error@0.0.2: @@ -26479,9 +26484,6 @@ packages: /zod@3.23.8: resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} - /zod@3.24.2: - resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==} - /zustand@4.5.6(react@18.3.1): resolution: {integrity: sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==} engines: {node: '>=12.7.0'}