diff --git a/.env.example b/.env.example index 45de2ad1e0..bff49baad5 100644 --- a/.env.example +++ b/.env.example @@ -194,9 +194,6 @@ REDIS_URL=redis://localhost:6379 # The below is used for Rate Limiting (uses In-Memory LRU Cache if not provided) (You can use a service like Webdis for this) # REDIS_HTTP_URL: -# The below is used for Rate Limiting for management API -UNKEY_ROOT_KEY= - # INTERCOM_APP_ID= # INTERCOM_SECRET_KEY= diff --git a/apps/web/lib/constants.ts b/apps/web/lib/constants.ts index 81f7b34b22..345421b80f 100644 --- a/apps/web/lib/constants.ts +++ b/apps/web/lib/constants.ts @@ -175,7 +175,6 @@ export const ENTERPRISE_LICENSE_KEY = env.ENTERPRISE_LICENSE_KEY; export const REDIS_URL = env.REDIS_URL; export const REDIS_HTTP_URL = env.REDIS_HTTP_URL; export const RATE_LIMITING_DISABLED = env.RATE_LIMITING_DISABLED === "1"; -export const UNKEY_ROOT_KEY = env.UNKEY_ROOT_KEY; export const BREVO_API_KEY = env.BREVO_API_KEY; export const BREVO_LIST_ID = env.BREVO_LIST_ID; diff --git a/apps/web/lib/env.ts b/apps/web/lib/env.ts index 3c39603ab4..1229e4d745 100644 --- a/apps/web/lib/env.ts +++ b/apps/web/lib/env.ts @@ -116,7 +116,7 @@ export const env = createEnv({ VERCEL_URL: z.string().optional(), WEBAPP_URL: z.string().url().optional(), UNSPLASH_ACCESS_KEY: z.string().optional(), - UNKEY_ROOT_KEY: z.string().optional(), + NODE_ENV: z.enum(["development", "production", "test"]).optional(), PROMETHEUS_EXPORTER_PORT: z.string().optional(), PROMETHEUS_ENABLED: z.enum(["1", "0"]).optional(), @@ -218,7 +218,6 @@ export const env = createEnv({ VERCEL_URL: process.env.VERCEL_URL, WEBAPP_URL: process.env.WEBAPP_URL, UNSPLASH_ACCESS_KEY: process.env.UNSPLASH_ACCESS_KEY, - UNKEY_ROOT_KEY: process.env.UNKEY_ROOT_KEY, NODE_ENV: process.env.NODE_ENV, PROMETHEUS_ENABLED: process.env.PROMETHEUS_ENABLED, PROMETHEUS_EXPORTER_PORT: process.env.PROMETHEUS_EXPORTER_PORT, diff --git a/apps/web/modules/api/v2/auth/api-wrapper.ts b/apps/web/modules/api/v2/auth/api-wrapper.ts index 77f1614a5e..b9028565cb 100644 --- a/apps/web/modules/api/v2/auth/api-wrapper.ts +++ b/apps/web/modules/api/v2/auth/api-wrapper.ts @@ -1,6 +1,7 @@ import { ApiAuditLog } from "@/app/lib/api/with-api-logging"; -import { checkRateLimitAndThrowError } from "@/modules/api/v2/lib/rate-limit"; import { formatZodError, handleApiError } from "@/modules/api/v2/lib/utils"; +import { applyRateLimit } from "@/modules/core/rate-limit/helpers"; +import { rateLimitConfigs } from "@/modules/core/rate-limit/rate-limit-configs"; import { ZodRawShape, z } from "zod"; import { TAuthenticationApiKey } from "@formbricks/types/auth"; import { authenticateRequest } from "./authenticate-request"; @@ -104,11 +105,10 @@ export const apiWrapper = async ({ } if (rateLimit) { - const rateLimitResponse = await checkRateLimitAndThrowError({ - identifier: authentication.data.hashedApiKey, - }); - if (!rateLimitResponse.ok) { - return handleApiError(request, rateLimitResponse.error); + try { + await applyRateLimit(rateLimitConfigs.api.v2, authentication.data.hashedApiKey); + } catch (error) { + return handleApiError(request, { type: "too_many_requests", details: error.message }); } } diff --git a/apps/web/modules/api/v2/auth/tests/api-wrapper.test.ts b/apps/web/modules/api/v2/auth/tests/api-wrapper.test.ts index 9903a83c6b..a5e7376551 100644 --- a/apps/web/modules/api/v2/auth/tests/api-wrapper.test.ts +++ b/apps/web/modules/api/v2/auth/tests/api-wrapper.test.ts @@ -1,22 +1,26 @@ import { apiWrapper } from "@/modules/api/v2/auth/api-wrapper"; import { authenticateRequest } from "@/modules/api/v2/auth/authenticate-request"; -import { checkRateLimitAndThrowError } from "@/modules/api/v2/lib/rate-limit"; import { handleApiError } from "@/modules/api/v2/lib/utils"; import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; +import { checkRateLimit } from "@/modules/core/rate-limit/rate-limit"; import { describe, expect, test, vi } from "vitest"; import { z } from "zod"; -import { err, ok, okVoid } from "@formbricks/types/error-handlers"; +import { err, ok } from "@formbricks/types/error-handlers"; vi.mock("../authenticate-request", () => ({ authenticateRequest: vi.fn(), })); -vi.mock("@/modules/api/v2/lib/rate-limit", () => ({ - checkRateLimitAndThrowError: vi.fn(), +vi.mock("@/modules/core/rate-limit/rate-limit", () => ({ + checkRateLimit: vi.fn(), })); -vi.mock("@/modules/api/v2/lib/utils", () => ({ - handleApiError: vi.fn(), +vi.mock("@/modules/core/rate-limit/rate-limit-configs", () => ({ + rateLimitConfigs: { + api: { + v2: { interval: 60, allowedPerInterval: 100, namespace: "api:v2" }, + }, + }, })); vi.mock("@/modules/api/v2/lib/utils", () => ({ @@ -24,20 +28,31 @@ vi.mock("@/modules/api/v2/lib/utils", () => ({ handleApiError: vi.fn(), })); +const mockAuthentication = { + type: "apiKey" as const, + environmentPermissions: [ + { + environmentId: "env-id", + environmentType: "development" as const, + projectId: "project-id", + projectName: "Project Name", + permission: "manage" as const, + }, + ], + hashedApiKey: "hashed-api-key", + apiKeyId: "api-key-id", + organizationId: "org-id", + organizationAccess: {} as any, +} as any; + describe("apiWrapper", () => { test("should handle request and return response", async () => { const request = new Request("http://localhost", { headers: { "x-api-key": "valid-api-key" }, }); - vi.mocked(authenticateRequest).mockResolvedValue( - ok({ - type: "apiKey", - environmentId: "env-id", - hashedApiKey: "hashed-api-key", - }) - ); - vi.mocked(checkRateLimitAndThrowError).mockResolvedValue(okVoid()); + vi.mocked(authenticateRequest).mockResolvedValue(ok(mockAuthentication)); + vi.mocked(checkRateLimit).mockResolvedValue(ok({ allowed: true })); const handler = vi.fn().mockResolvedValue(new Response("ok", { status: 200 })); const response = await apiWrapper({ @@ -74,13 +89,7 @@ describe("apiWrapper", () => { headers: { "Content-Type": "application/json" }, }); - vi.mocked(authenticateRequest).mockResolvedValue( - ok({ - type: "apiKey", - environmentId: "env-id", - hashedApiKey: "hashed-api-key", - }) - ); + vi.mocked(authenticateRequest).mockResolvedValue(ok(mockAuthentication)); const bodySchema = z.object({ key: z.string() }); const handler = vi.fn().mockResolvedValue(new Response("ok", { status: 200 })); @@ -107,14 +116,7 @@ describe("apiWrapper", () => { headers: { "Content-Type": "application/json" }, }); - vi.mocked(authenticateRequest).mockResolvedValue( - ok({ - type: "apiKey", - environmentId: "env-id", - hashedApiKey: "hashed-api-key", - }) - ); - + vi.mocked(authenticateRequest).mockResolvedValue(ok(mockAuthentication)); vi.mocked(handleApiError).mockResolvedValue(new Response("error", { status: 400 })); const bodySchema = z.object({ key: z.string() }); @@ -134,13 +136,7 @@ describe("apiWrapper", () => { test("should parse query schema correctly", async () => { const request = new Request("http://localhost?key=value"); - vi.mocked(authenticateRequest).mockResolvedValue( - ok({ - type: "apiKey", - environmentId: "env-id", - hashedApiKey: "hashed-api-key", - }) - ); + vi.mocked(authenticateRequest).mockResolvedValue(ok(mockAuthentication)); const querySchema = z.object({ key: z.string() }); const handler = vi.fn().mockResolvedValue(new Response("ok", { status: 200 })); @@ -163,14 +159,7 @@ describe("apiWrapper", () => { test("should handle query schema errors", async () => { const request = new Request("http://localhost?foo%ZZ=abc"); - vi.mocked(authenticateRequest).mockResolvedValue( - ok({ - type: "apiKey", - environmentId: "env-id", - hashedApiKey: "hashed-api-key", - }) - ); - + vi.mocked(authenticateRequest).mockResolvedValue(ok(mockAuthentication)); vi.mocked(handleApiError).mockResolvedValue(new Response("error", { status: 400 })); const querySchema = z.object({ key: z.string() }); @@ -190,13 +179,7 @@ describe("apiWrapper", () => { test("should parse params schema correctly", async () => { const request = new Request("http://localhost"); - vi.mocked(authenticateRequest).mockResolvedValue( - ok({ - type: "apiKey", - environmentId: "env-id", - hashedApiKey: "hashed-api-key", - }) - ); + vi.mocked(authenticateRequest).mockResolvedValue(ok(mockAuthentication)); const paramsSchema = z.object({ key: z.string() }); const handler = vi.fn().mockResolvedValue(new Response("ok", { status: 200 })); @@ -220,14 +203,7 @@ describe("apiWrapper", () => { test("should handle no external params", async () => { const request = new Request("http://localhost"); - vi.mocked(authenticateRequest).mockResolvedValue( - ok({ - type: "apiKey", - environmentId: "env-id", - hashedApiKey: "hashed-api-key", - }) - ); - + vi.mocked(authenticateRequest).mockResolvedValue(ok(mockAuthentication)); vi.mocked(handleApiError).mockResolvedValue(new Response("error", { status: 400 })); const paramsSchema = z.object({ key: z.string() }); @@ -248,14 +224,7 @@ describe("apiWrapper", () => { test("should handle params schema errors", async () => { const request = new Request("http://localhost"); - vi.mocked(authenticateRequest).mockResolvedValue( - ok({ - type: "apiKey", - environmentId: "env-id", - hashedApiKey: "hashed-api-key", - }) - ); - + vi.mocked(authenticateRequest).mockResolvedValue(ok(mockAuthentication)); vi.mocked(handleApiError).mockResolvedValue(new Response("error", { status: 400 })); const paramsSchema = z.object({ key: z.string() }); @@ -273,21 +242,13 @@ describe("apiWrapper", () => { expect(handler).not.toHaveBeenCalled(); }); - test("should handle rate limit errors", async () => { + test("should handle rate limit exceeded", async () => { const request = new Request("http://localhost", { headers: { "x-api-key": "valid-api-key" }, }); - vi.mocked(authenticateRequest).mockResolvedValue( - ok({ - type: "apiKey", - environmentId: "env-id", - hashedApiKey: "hashed-api-key", - }) - ); - vi.mocked(checkRateLimitAndThrowError).mockResolvedValue( - err({ type: "rateLimitExceeded" } as unknown as ApiErrorResponseV2) - ); + vi.mocked(authenticateRequest).mockResolvedValue(ok(mockAuthentication)); + vi.mocked(checkRateLimit).mockResolvedValue(ok({ allowed: false })); vi.mocked(handleApiError).mockImplementation( (_request: Request, _error: ApiErrorResponseV2): Response => new Response("rate limit exceeded", { status: 429 }) @@ -302,4 +263,24 @@ describe("apiWrapper", () => { expect(response.status).toBe(429); expect(handler).not.toHaveBeenCalled(); }); + + test("should handle rate limit check failure gracefully", async () => { + const request = new Request("http://localhost", { + headers: { "x-api-key": "valid-api-key" }, + }); + + vi.mocked(authenticateRequest).mockResolvedValue(ok(mockAuthentication)); + // When rate limiting fails (e.g., Redis connection issues), checkRateLimit fails open by returning allowed: true + vi.mocked(checkRateLimit).mockResolvedValue(ok({ allowed: true })); + + const handler = vi.fn().mockResolvedValue(new Response("ok", { status: 200 })); + const response = await apiWrapper({ + request, + handler, + }); + + // Should fail open for availability + expect(response.status).toBe(200); + expect(handler).toHaveBeenCalled(); + }); }); diff --git a/apps/web/modules/api/v2/lib/rate-limit.ts b/apps/web/modules/api/v2/lib/rate-limit.ts deleted file mode 100644 index 0ebf99b183..0000000000 --- a/apps/web/modules/api/v2/lib/rate-limit.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { MANAGEMENT_API_RATE_LIMIT, RATE_LIMITING_DISABLED, UNKEY_ROOT_KEY } from "@/lib/constants"; -import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; -import { type LimitOptions, Ratelimit, type RatelimitResponse } from "@unkey/ratelimit"; -import { logger } from "@formbricks/logger"; -import { Result, err, okVoid } from "@formbricks/types/error-handlers"; - -export type RateLimitHelper = { - identifier: string; - opts?: LimitOptions; - /** - * Using a callback instead of a regular return to provide headers even - * when the rate limit is reached and an error is thrown. - **/ - onRateLimiterResponse?: (response: RatelimitResponse) => void; -}; - -let warningDisplayed = false; - -/** Prevent flooding the logs while testing/building */ -function logOnce(message: string) { - if (warningDisplayed) return; - logger.warn(message); - warningDisplayed = true; -} - -export function rateLimiter() { - if (RATE_LIMITING_DISABLED) { - logOnce("Rate limiting disabled"); - return () => ({ success: true, limit: 10, remaining: 999, reset: 0 }) as RatelimitResponse; - } - - if (!UNKEY_ROOT_KEY) { - logOnce("Disabled due to not finding UNKEY_ROOT_KEY env variable"); - return () => ({ success: true, limit: 10, remaining: 999, reset: 0 }) as RatelimitResponse; - } - const timeout = { - fallback: { success: true, limit: 10, remaining: 999, reset: 0 }, - ms: 5000, - }; - - const limiter = { - api: new Ratelimit({ - rootKey: UNKEY_ROOT_KEY, - namespace: "api", - limit: MANAGEMENT_API_RATE_LIMIT.allowedPerInterval, - duration: MANAGEMENT_API_RATE_LIMIT.interval * 1000, - timeout, - }), - }; - - async function rateLimit({ identifier, opts }: RateLimitHelper) { - return await limiter.api.limit(identifier, opts); - } - - return rateLimit; -} - -export const checkRateLimitAndThrowError = async ({ - identifier, - opts, -}: RateLimitHelper): Promise> => { - const response = await rateLimiter()({ identifier, opts }); - const { success } = response; - - if (!success) { - return err({ - type: "too_many_requests", - }); - } - return okVoid(); -}; diff --git a/apps/web/modules/api/v2/lib/tests/rate-limit.test.ts b/apps/web/modules/api/v2/lib/tests/rate-limit.test.ts deleted file mode 100644 index 5b1f70aa41..0000000000 --- a/apps/web/modules/api/v2/lib/tests/rate-limit.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { beforeEach, describe, expect, test, vi } from "vitest"; -import { logger } from "@formbricks/logger"; - -vi.mock("@formbricks/logger", () => ({ - logger: { - warn: vi.fn(), - }, -})); - -vi.mock("@unkey/ratelimit", () => ({ - Ratelimit: vi.fn(), -})); - -describe("when rate limiting is disabled", () => { - beforeEach(async () => { - vi.resetModules(); - const constants = await vi.importActual("@/lib/constants"); - vi.doMock("@/lib/constants", () => ({ - ...constants, - MANAGEMENT_API_RATE_LIMIT: { allowedPerInterval: 5, interval: 60 }, - RATE_LIMITING_DISABLED: true, - })); - }); - - test("should log a warning once and return a stubbed response", async () => { - const loggerSpy = vi.spyOn(logger, "warn"); - const { rateLimiter } = await import("@/modules/api/v2/lib/rate-limit"); - - const res1 = await rateLimiter()({ identifier: "test-id" }); - expect(res1).toEqual({ success: true, limit: 10, remaining: 999, reset: 0 }); - expect(loggerSpy).toHaveBeenCalled(); - - // Subsequent calls won't log again. - await rateLimiter()({ identifier: "another-id" }); - - expect(loggerSpy).toHaveBeenCalledTimes(1); - loggerSpy.mockRestore(); - }); -}); - -describe("when UNKEY_ROOT_KEY is missing", () => { - beforeEach(async () => { - vi.resetModules(); - const constants = await vi.importActual("@/lib/constants"); - vi.doMock("@/lib/constants", () => ({ - ...constants, - MANAGEMENT_API_RATE_LIMIT: { allowedPerInterval: 5, interval: 60 }, - RATE_LIMITING_DISABLED: false, - UNKEY_ROOT_KEY: "", - })); - }); - - test("should log a warning about missing UNKEY_ROOT_KEY and return stub response", async () => { - const loggerSpy = vi.spyOn(logger, "warn"); - const { rateLimiter } = await import("@/modules/api/v2/lib/rate-limit"); - const limiterFunc = rateLimiter(); - - const res = await limiterFunc({ identifier: "test-id" }); - expect(res).toEqual({ success: true, limit: 10, remaining: 999, reset: 0 }); - expect(loggerSpy).toHaveBeenCalled(); - loggerSpy.mockRestore(); - }); -}); - -describe("when rate limiting is active (enabled)", () => { - const mockResponse = { success: true, limit: 5, remaining: 2, reset: 1000 }; - let limitMock: ReturnType; - - beforeEach(async () => { - vi.resetModules(); - const constants = await vi.importActual("@/lib/constants"); - vi.doMock("@/lib/constants", () => ({ - ...constants, - MANAGEMENT_API_RATE_LIMIT: { allowedPerInterval: 5, interval: 60 }, - RATE_LIMITING_DISABLED: false, - UNKEY_ROOT_KEY: "valid-key", - })); - - limitMock = vi.fn().mockResolvedValue(mockResponse); - const RatelimitMock = vi.fn().mockImplementation(() => { - return { limit: limitMock }; - }); - vi.doMock("@unkey/ratelimit", () => ({ - Ratelimit: RatelimitMock, - })); - }); - - test("should create a rate limiter that calls the limit method with the proper arguments", async () => { - const { rateLimiter } = await import("../rate-limit"); - const limiterFunc = rateLimiter(); - const res = await limiterFunc({ identifier: "abc", opts: { cost: 1 } }); - expect(limitMock).toHaveBeenCalledWith("abc", { cost: 1 }); - expect(res).toEqual(mockResponse); - }); - - test("checkRateLimitAndThrowError returns okVoid when rate limit is not exceeded", async () => { - limitMock.mockResolvedValueOnce({ success: true, limit: 5, remaining: 3, reset: 1000 }); - - const { checkRateLimitAndThrowError } = await import("../rate-limit"); - const result = await checkRateLimitAndThrowError({ identifier: "abc" }); - expect(result.ok).toBe(true); - }); - - test("checkRateLimitAndThrowError returns an error when the rate limit is exceeded", async () => { - limitMock.mockResolvedValueOnce({ success: false, limit: 5, remaining: 0, reset: 1000 }); - - const { checkRateLimitAndThrowError } = await import("../rate-limit"); - const result = await checkRateLimitAndThrowError({ identifier: "abc" }); - expect(result.ok).toBe(false); - if (!result.ok) { - expect(result.error).toEqual({ type: "too_many_requests" }); - } - }); -}); diff --git a/apps/web/package.json b/apps/web/package.json index 69ab053a8c..2f7666b3ad 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -79,7 +79,6 @@ "@tolgee/format-icu": "6.2.5", "@tolgee/react": "6.2.5", "@ungap/structured-clone": "1.3.0", - "@unkey/ratelimit": "0.5.5", "@vercel/functions": "2.0.2", "@vercel/og": "0.6.8", "bcryptjs": "3.0.2", diff --git a/docs/self-hosting/configuration/environment-variables.mdx b/docs/self-hosting/configuration/environment-variables.mdx index 0e4c227431..69707ce230 100644 --- a/docs/self-hosting/configuration/environment-variables.mdx +++ b/docs/self-hosting/configuration/environment-variables.mdx @@ -54,7 +54,7 @@ These variables are present inside your machine's docker-compose file. Restart t | STRIPE_SECRET_KEY | Secret key for Stripe integration. | optional | | | STRIPE_WEBHOOK_SECRET | Webhook secret for Stripe integration. | optional | | | TELEMETRY_DISABLED | Disables telemetry if set to 1. | optional | | -| DEFAULT_BRAND_COLOR | Default brand color for your app (Can be overwritten from the UI as well). | optional | #64748b | +| DEFAULT_BRAND_COLOR | Default brand color for your app (Can be overwritten from the UI as well). | optional | #64748b | | DEFAULT_ORGANIZATION_ID | Automatically assign new users to a specific organization when joining | optional | | | OIDC_DISPLAY_NAME | Display name for Custom OpenID Connect Provider | optional | | | OIDC_CLIENT_ID | Client ID for Custom OpenID Connect Provider | optional (required if OIDC auth is enabled) | | @@ -62,7 +62,6 @@ These variables are present inside your machine's docker-compose file. Restart t | OIDC_ISSUER | Issuer URL for Custom OpenID Connect Provider (should have .well-known configured at this) | optional (required if OIDC auth is enabled) | | | OIDC_SIGNING_ALGORITHM | Signing Algorithm for Custom OpenID Connect Provider | optional | RS256 | | OPENTELEMETRY_LISTENER_URL | URL for OpenTelemetry listener inside Formbricks. | optional | | -| UNKEY_ROOT_KEY | Key for the [Unkey](https://www.unkey.com/) service. This is used for Rate Limiting for management API. | optional | | | PROMETHEUS_ENABLED | Enables Prometheus metrics if set to 1. | optional | | | PROMETHEUS_EXPORTER_PORT | Port for Prometheus metrics. | optional | 9090 | | DOCKER_CRON_ENABLED | Controls whether cron jobs run in the Docker image. Set to 0 to disable (useful for cluster setups). | optional | 1 | diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 788fbf1a04..0072f78c60 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -277,9 +277,6 @@ importers: '@ungap/structured-clone': specifier: 1.3.0 version: 1.3.0 - '@unkey/ratelimit': - specifier: 0.5.5 - version: 0.5.5 '@vercel/functions': specifier: 2.0.2 version: 2.0.2(@aws-sdk/credential-provider-web-identity@3.804.0) @@ -4339,18 +4336,6 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - '@unkey/api@0.33.1': - resolution: {integrity: sha512-HXFQOGjO3S0+N6GMBOz9lkn2Fp41rdgNrafBkjRg8IjOsNv6SNdWrXZSELONinZYycr7Wxc/t7Vhx7xUdb0cdA==} - - '@unkey/error@0.2.0': - resolution: {integrity: sha512-DFGb4A7SrusZPP0FYuRIF0CO+Gi4etLUAEJ6EKc+TKYmscL0nEJ2Pr38FyX9MvjI4Wx5l35Wc9KsBjMm9Ybh7w==} - - '@unkey/ratelimit@0.5.5': - resolution: {integrity: sha512-79Xv7lZFHqScGzBQpO3hh2SZBhujXDDkyH/izWzeXcJ5sFYO+PsC+VaAFagEinbHbrx116RkIRIqbDrfZfRpxA==} - - '@unkey/rbac@0.3.1': - resolution: {integrity: sha512-Hj+52XRIlBBl3/qOUq9K71Fwy3PWExBQOpOClVYHdrcmbgqNL6L4EdW/BzliLhqPCdwZTPVSJTnZ3Hw4ZYixsQ==} - '@unrs/resolver-binding-android-arm-eabi@1.11.1': resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} cpu: [arm] @@ -14435,23 +14420,6 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@unkey/api@0.33.1': - dependencies: - '@unkey/rbac': 0.3.1 - - '@unkey/error@0.2.0': - dependencies: - zod: 3.24.4 - - '@unkey/ratelimit@0.5.5': - dependencies: - '@unkey/api': 0.33.1 - - '@unkey/rbac@0.3.1': - dependencies: - '@unkey/error': 0.2.0 - zod: 3.24.4 - '@unrs/resolver-binding-android-arm-eabi@1.11.1': optional: true diff --git a/turbo.json b/turbo.json index 3f6f9b95f9..5fe11d82ed 100644 --- a/turbo.json +++ b/turbo.json @@ -174,7 +174,6 @@ "VERSION", "WEBAPP_URL", "UNSPLASH_ACCESS_KEY", - "UNKEY_ROOT_KEY", "PROMETHEUS_ENABLED", "PROMETHEUS_EXPORTER_PORT", "USER_MANAGEMENT_MINIMUM_ROLE"