mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-05 19:30:48 -05:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 056a019738 | |||
| 9d2e988c59 |
@@ -157,9 +157,7 @@ const handleApiKeyAuthentication = async (apiKey: string) => {
|
||||
});
|
||||
}
|
||||
|
||||
const rateLimitError = await checkRateLimit(apiKeyData.id);
|
||||
if (rateLimitError) return rateLimitError;
|
||||
|
||||
// Rate limiting for apiKey auth is enforced by Envoy in v5 — see envoy-rate-limit-coverage.ts
|
||||
if (!isValidApiKeyEnvironment(apiKeyData)) {
|
||||
return responses.badRequestResponse("You can't use this method with this API key");
|
||||
}
|
||||
|
||||
@@ -3,9 +3,16 @@ import { NextRequest } from "next/server";
|
||||
import { Mock, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { TAuthenticationApiKey } from "@formbricks/types/auth";
|
||||
import { AuthenticationMethod } from "@/app/middleware/endpoint-validator";
|
||||
import type { AuthenticationMethod } from "@/app/middleware/endpoint-validator";
|
||||
import { responses } from "./response";
|
||||
|
||||
const AuthMethod = {
|
||||
ApiKey: "apiKey" as AuthenticationMethod,
|
||||
Session: "session" as AuthenticationMethod,
|
||||
Both: "both" as AuthenticationMethod,
|
||||
None: "none" as AuthenticationMethod,
|
||||
} as const;
|
||||
|
||||
vi.mock("@/modules/ee/audit-logs/lib/handler", () => ({
|
||||
__esModule: true,
|
||||
queueAuditEvent: vi.fn(),
|
||||
@@ -122,7 +129,7 @@ describe("withV1ApiWrapper", () => {
|
||||
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
|
||||
vi.mocked(isManagementApiRoute).mockReturnValue({
|
||||
isManagementApi: true,
|
||||
authenticationMethod: AuthenticationMethod.ApiKey,
|
||||
authenticationMethod: AuthMethod.ApiKey,
|
||||
});
|
||||
vi.mocked(isIntegrationRoute).mockReturnValue(false);
|
||||
|
||||
@@ -198,7 +205,7 @@ describe("withV1ApiWrapper", () => {
|
||||
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
|
||||
vi.mocked(isManagementApiRoute).mockReturnValue({
|
||||
isManagementApi: true,
|
||||
authenticationMethod: AuthenticationMethod.ApiKey,
|
||||
authenticationMethod: AuthMethod.ApiKey,
|
||||
});
|
||||
vi.mocked(isIntegrationRoute).mockReturnValue(false);
|
||||
|
||||
@@ -244,7 +251,7 @@ describe("withV1ApiWrapper", () => {
|
||||
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
|
||||
vi.mocked(isManagementApiRoute).mockReturnValue({
|
||||
isManagementApi: true,
|
||||
authenticationMethod: AuthenticationMethod.ApiKey,
|
||||
authenticationMethod: AuthMethod.ApiKey,
|
||||
});
|
||||
vi.mocked(isIntegrationRoute).mockReturnValue(false);
|
||||
|
||||
@@ -318,7 +325,7 @@ describe("withV1ApiWrapper", () => {
|
||||
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
|
||||
vi.mocked(isManagementApiRoute).mockReturnValue({
|
||||
isManagementApi: true,
|
||||
authenticationMethod: AuthenticationMethod.ApiKey,
|
||||
authenticationMethod: AuthMethod.ApiKey,
|
||||
});
|
||||
vi.mocked(isIntegrationRoute).mockReturnValue(false);
|
||||
|
||||
@@ -370,7 +377,7 @@ describe("withV1ApiWrapper", () => {
|
||||
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
|
||||
vi.mocked(isManagementApiRoute).mockReturnValue({
|
||||
isManagementApi: true,
|
||||
authenticationMethod: AuthenticationMethod.ApiKey,
|
||||
authenticationMethod: AuthMethod.ApiKey,
|
||||
});
|
||||
vi.mocked(isIntegrationRoute).mockReturnValue(false);
|
||||
|
||||
@@ -425,7 +432,7 @@ describe("withV1ApiWrapper", () => {
|
||||
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
|
||||
vi.mocked(isManagementApiRoute).mockReturnValue({
|
||||
isManagementApi: true,
|
||||
authenticationMethod: AuthenticationMethod.ApiKey,
|
||||
authenticationMethod: AuthMethod.ApiKey,
|
||||
});
|
||||
vi.mocked(isIntegrationRoute).mockReturnValue(false);
|
||||
|
||||
@@ -449,7 +456,7 @@ describe("withV1ApiWrapper", () => {
|
||||
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: true, isRateLimited: true });
|
||||
vi.mocked(isManagementApiRoute).mockReturnValue({
|
||||
isManagementApi: false,
|
||||
authenticationMethod: AuthenticationMethod.None,
|
||||
authenticationMethod: AuthMethod.None,
|
||||
});
|
||||
vi.mocked(isIntegrationRoute).mockReturnValue(false);
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(null);
|
||||
@@ -473,6 +480,90 @@ describe("withV1ApiWrapper", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("skips app rate limiting for Envoy-covered client routes", async () => {
|
||||
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
|
||||
await import("@/app/middleware/endpoint-validator");
|
||||
const { authenticateRequest } = await import("@/app/api/v1/auth");
|
||||
const { applyIPRateLimit } = await import("@/modules/core/rate-limit/helpers");
|
||||
|
||||
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: true, isRateLimited: true });
|
||||
vi.mocked(isManagementApiRoute).mockReturnValue({
|
||||
isManagementApi: false,
|
||||
authenticationMethod: AuthMethod.None,
|
||||
});
|
||||
vi.mocked(isIntegrationRoute).mockReturnValue(false);
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(null);
|
||||
vi.mocked(applyIPRateLimit).mockResolvedValue({ allowed: true });
|
||||
|
||||
const handler = vi.fn().mockResolvedValue({
|
||||
response: responses.successResponse({ data: "test" }),
|
||||
});
|
||||
|
||||
const req = createMockRequest({ method: "POST", url: "/api/v1/client/env_123/storage" });
|
||||
const { withV1ApiWrapper } = await import("./with-api-logging");
|
||||
const wrapped = withV1ApiWrapper({ handler });
|
||||
const res = await wrapped(req, undefined);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(applyIPRateLimit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("keeps app rate limiting for uncovered client routes", async () => {
|
||||
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
|
||||
await import("@/app/middleware/endpoint-validator");
|
||||
const { authenticateRequest } = await import("@/app/api/v1/auth");
|
||||
const { applyIPRateLimit } = await import("@/modules/core/rate-limit/helpers");
|
||||
|
||||
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: true, isRateLimited: true });
|
||||
vi.mocked(isManagementApiRoute).mockReturnValue({
|
||||
isManagementApi: false,
|
||||
authenticationMethod: AuthMethod.None,
|
||||
});
|
||||
vi.mocked(isIntegrationRoute).mockReturnValue(false);
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(null);
|
||||
vi.mocked(applyIPRateLimit).mockResolvedValue({ allowed: true });
|
||||
|
||||
const handler = vi.fn().mockResolvedValue({
|
||||
response: responses.successResponse({ data: "test" }),
|
||||
});
|
||||
|
||||
const req = createMockRequest({ method: "GET", url: "/api/v2/client/env_123/environment" });
|
||||
const { withV1ApiWrapper } = await import("./with-api-logging");
|
||||
const wrapped = withV1ApiWrapper({ handler });
|
||||
const res = await wrapped(req, undefined);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(applyIPRateLimit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("keeps app rate limiting for uncovered verbs on otherwise covered client paths", async () => {
|
||||
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
|
||||
await import("@/app/middleware/endpoint-validator");
|
||||
const { authenticateRequest } = await import("@/app/api/v1/auth");
|
||||
const { applyIPRateLimit } = await import("@/modules/core/rate-limit/helpers");
|
||||
|
||||
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: true, isRateLimited: true });
|
||||
vi.mocked(isManagementApiRoute).mockReturnValue({
|
||||
isManagementApi: false,
|
||||
authenticationMethod: AuthMethod.None,
|
||||
});
|
||||
vi.mocked(isIntegrationRoute).mockReturnValue(false);
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(null);
|
||||
vi.mocked(applyIPRateLimit).mockResolvedValue({ allowed: true });
|
||||
|
||||
const handler = vi.fn().mockResolvedValue({
|
||||
response: responses.successResponse({ data: "test" }),
|
||||
});
|
||||
|
||||
const req = createMockRequest({ method: "PATCH", url: "/api/v1/client/env_123/environment" });
|
||||
const { withV1ApiWrapper } = await import("./with-api-logging");
|
||||
const wrapped = withV1ApiWrapper({ handler });
|
||||
const res = await wrapped(req, undefined);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(applyIPRateLimit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("returns authentication error for non-client routes without auth", async () => {
|
||||
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
|
||||
await import("@/app/middleware/endpoint-validator");
|
||||
@@ -481,7 +572,7 @@ describe("withV1ApiWrapper", () => {
|
||||
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
|
||||
vi.mocked(isManagementApiRoute).mockReturnValue({
|
||||
isManagementApi: true,
|
||||
authenticationMethod: AuthenticationMethod.ApiKey,
|
||||
authenticationMethod: AuthMethod.ApiKey,
|
||||
});
|
||||
vi.mocked(isIntegrationRoute).mockReturnValue(false);
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(null);
|
||||
@@ -504,7 +595,7 @@ describe("withV1ApiWrapper", () => {
|
||||
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
|
||||
vi.mocked(isManagementApiRoute).mockReturnValue({
|
||||
isManagementApi: true,
|
||||
authenticationMethod: AuthenticationMethod.Session,
|
||||
authenticationMethod: AuthMethod.Session,
|
||||
});
|
||||
vi.mocked(isIntegrationRoute).mockReturnValue(false);
|
||||
vi.mocked(getServerSession).mockResolvedValue(null);
|
||||
@@ -528,7 +619,36 @@ describe("withV1ApiWrapper", () => {
|
||||
expect(mockContextualLoggerError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("handles rate limiting errors", async () => {
|
||||
test("keeps app rate limiting for uncovered session-authenticated management routes", async () => {
|
||||
const { applyRateLimit } = await import("@/modules/core/rate-limit/helpers");
|
||||
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
|
||||
await import("@/app/middleware/endpoint-validator");
|
||||
const { getServerSession } = await import("next-auth");
|
||||
|
||||
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
|
||||
vi.mocked(isManagementApiRoute).mockReturnValue({
|
||||
isManagementApi: true,
|
||||
authenticationMethod: AuthMethod.Both,
|
||||
});
|
||||
vi.mocked(isIntegrationRoute).mockReturnValue(false);
|
||||
vi.mocked(getServerSession).mockResolvedValue({ user: { id: "user-1" } } as any);
|
||||
const rateLimitError = new Error("Rate limit exceeded");
|
||||
rateLimitError.message = "Rate limit exceeded";
|
||||
vi.mocked(applyRateLimit).mockRejectedValue(rateLimitError);
|
||||
|
||||
const handler = vi.fn();
|
||||
const req = createMockRequest({ method: "POST", url: "https://api.test/api/v1/management/storage" });
|
||||
const { withV1ApiWrapper } = await import("./with-api-logging");
|
||||
const customRateLimitConfig = { interval: 60, allowedPerInterval: 5, namespace: "storage:upload" };
|
||||
const wrapped = withV1ApiWrapper({ handler, customRateLimitConfig });
|
||||
const res = await wrapped(req, undefined);
|
||||
|
||||
expect(res.status).toBe(429);
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
expect(applyRateLimit).toHaveBeenCalledWith(customRateLimitConfig, "user-1");
|
||||
});
|
||||
|
||||
test("skips app rate limiting for Envoy-covered API-key management routes", async () => {
|
||||
const { applyRateLimit } = await import("@/modules/core/rate-limit/helpers");
|
||||
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
|
||||
await import("@/app/middleware/endpoint-validator");
|
||||
@@ -538,21 +658,22 @@ describe("withV1ApiWrapper", () => {
|
||||
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
|
||||
vi.mocked(isManagementApiRoute).mockReturnValue({
|
||||
isManagementApi: true,
|
||||
authenticationMethod: AuthenticationMethod.ApiKey,
|
||||
authenticationMethod: AuthMethod.ApiKey,
|
||||
});
|
||||
vi.mocked(isIntegrationRoute).mockReturnValue(false);
|
||||
const rateLimitError = new Error("Rate limit exceeded");
|
||||
rateLimitError.message = "Rate limit exceeded";
|
||||
vi.mocked(applyRateLimit).mockRejectedValue(rateLimitError);
|
||||
vi.mocked(applyRateLimit).mockResolvedValue({ allowed: true });
|
||||
|
||||
const handler = vi.fn().mockResolvedValue({
|
||||
response: responses.successResponse({ data: "test" }),
|
||||
});
|
||||
|
||||
const handler = vi.fn();
|
||||
const req = createMockRequest({ url: V1_MANAGEMENT_SURVEYS_URL });
|
||||
const { withV1ApiWrapper } = await import("./with-api-logging");
|
||||
const wrapped = withV1ApiWrapper({ handler });
|
||||
const res = await wrapped(req, undefined);
|
||||
|
||||
expect(res.status).toBe(429);
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
expect(res.status).toBe(200);
|
||||
expect(applyRateLimit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("skips audit log creation when no action/targetType provided", async () => {
|
||||
@@ -566,7 +687,7 @@ describe("withV1ApiWrapper", () => {
|
||||
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
|
||||
vi.mocked(isManagementApiRoute).mockReturnValue({
|
||||
isManagementApi: true,
|
||||
authenticationMethod: AuthenticationMethod.ApiKey,
|
||||
authenticationMethod: AuthMethod.ApiKey,
|
||||
});
|
||||
vi.mocked(isIntegrationRoute).mockReturnValue(false);
|
||||
|
||||
|
||||
@@ -13,6 +13,10 @@ import {
|
||||
} from "@/app/middleware/endpoint-validator";
|
||||
import { AUDIT_LOG_ENABLED } from "@/lib/constants";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import {
|
||||
TEnvoyRateLimitAuthType,
|
||||
isRouteRateLimitedByEnvoy,
|
||||
} from "@/modules/core/rate-limit/envoy-rate-limit-coverage";
|
||||
import { applyIPRateLimit, applyRateLimit } from "@/modules/core/rate-limit/helpers";
|
||||
import { rateLimitConfigs } from "@/modules/core/rate-limit/rate-limit-configs";
|
||||
import { TRateLimitConfig } from "@/modules/core/rate-limit/types/rate-limit";
|
||||
@@ -61,29 +65,58 @@ const applyClientRateLimit = async (customRateLimitConfig?: TRateLimitConfig): P
|
||||
await applyIPRateLimit(customRateLimitConfig ?? rateLimitConfigs.api.client);
|
||||
};
|
||||
|
||||
const getEnvoyRateLimitAuthType = (
|
||||
authentication: TApiV1Authentication
|
||||
): TEnvoyRateLimitAuthType | "unknown" => {
|
||||
if (!authentication) {
|
||||
return "none";
|
||||
}
|
||||
|
||||
if ("user" in authentication) {
|
||||
return "session";
|
||||
}
|
||||
|
||||
if ("apiKeyId" in authentication) {
|
||||
return "apiKey";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle rate limiting based on authentication and API type
|
||||
*/
|
||||
const handleRateLimiting = async (
|
||||
req: NextRequest,
|
||||
authentication: TApiV1Authentication,
|
||||
routeType: ApiV1RouteTypeEnum,
|
||||
customRateLimitConfig?: TRateLimitConfig
|
||||
): Promise<Response | null> => {
|
||||
const authType = getEnvoyRateLimitAuthType(authentication);
|
||||
|
||||
if (authType === "unknown") {
|
||||
logger.error({ authentication }, "Unknown authentication type");
|
||||
return responses.internalServerErrorResponse("Invalid authentication configuration");
|
||||
}
|
||||
|
||||
const isEnvoyManagedRateLimit = isRouteRateLimitedByEnvoy({
|
||||
pathname: req.nextUrl.pathname,
|
||||
method: req.method,
|
||||
authType,
|
||||
});
|
||||
|
||||
try {
|
||||
if (authentication) {
|
||||
if (authentication && !isEnvoyManagedRateLimit) {
|
||||
if ("user" in authentication) {
|
||||
// Session-based authentication for integration routes
|
||||
await applyRateLimit(customRateLimitConfig ?? rateLimitConfigs.api.v1, authentication.user.id);
|
||||
} else if ("apiKeyId" in authentication) {
|
||||
// API key authentication for general routes
|
||||
await applyRateLimit(customRateLimitConfig ?? rateLimitConfigs.api.v1, authentication.apiKeyId);
|
||||
} else {
|
||||
logger.error({ authentication }, "Unknown authentication type");
|
||||
return responses.internalServerErrorResponse("Invalid authentication configuration");
|
||||
}
|
||||
}
|
||||
|
||||
if (routeType === ApiV1RouteTypeEnum.Client) {
|
||||
if (routeType === ApiV1RouteTypeEnum.Client && !isEnvoyManagedRateLimit) {
|
||||
await applyClientRateLimit(customRateLimitConfig);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -286,7 +319,12 @@ export const withV1ApiWrapper = <TResult extends { response: Response; error?: u
|
||||
|
||||
// === Rate Limiting ===
|
||||
if (isRateLimited) {
|
||||
const rateLimitResponse = await handleRateLimiting(authentication, routeType, customRateLimitConfig);
|
||||
const rateLimitResponse = await handleRateLimiting(
|
||||
req,
|
||||
authentication,
|
||||
routeType,
|
||||
customRateLimitConfig
|
||||
);
|
||||
if (rateLimitResponse) return rateLimitResponse;
|
||||
}
|
||||
|
||||
|
||||
@@ -121,13 +121,10 @@ export const DELETE = async (
|
||||
: responses.notAuthenticatedResponse();
|
||||
}
|
||||
|
||||
if (authResult.ok) {
|
||||
// Rate limiting for apiKey DELETE is enforced by Envoy in v5 — see envoy-rate-limit-coverage.ts
|
||||
if (authResult.ok && authResult.data.authType !== "apiKey") {
|
||||
try {
|
||||
if (authResult.data.authType === "apiKey") {
|
||||
await applyRateLimit(rateLimitConfigs.storage.delete, authResult.data.apiKeyId);
|
||||
} else {
|
||||
await applyRateLimit(rateLimitConfigs.storage.delete, authResult.data.userId);
|
||||
}
|
||||
await applyRateLimit(rateLimitConfigs.storage.delete, authResult.data.userId);
|
||||
} catch (error) {
|
||||
return responses.tooManyRequestsResponse(
|
||||
error instanceof Error ? error.message : "Unknown error occurred"
|
||||
|
||||
@@ -6,7 +6,6 @@ import { EMAIL_VERIFICATION_DISABLED } from "@/lib/constants";
|
||||
import { capturePostHogEvent } from "@/lib/posthog";
|
||||
// Import mocked rate limiting functions
|
||||
import { applyIPRateLimit } from "@/modules/core/rate-limit/helpers";
|
||||
import { rateLimitConfigs } from "@/modules/core/rate-limit/rate-limit-configs";
|
||||
import { authOptions } from "./authOptions";
|
||||
import { mockUser } from "./mock-data";
|
||||
import { hashPassword } from "./utils";
|
||||
@@ -220,8 +219,8 @@ describe("authOptions", () => {
|
||||
});
|
||||
}, 15000);
|
||||
|
||||
describe("Rate Limiting", () => {
|
||||
test("should apply rate limiting before credential validation", async () => {
|
||||
describe("Envoy-managed callback behavior", () => {
|
||||
test("should not apply in-app rate limiting before credential validation", async () => {
|
||||
vi.mocked(applyIPRateLimit).mockResolvedValue({ allowed: true });
|
||||
vi.spyOn(prisma.user, "findUnique").mockResolvedValue({
|
||||
id: mockUserId,
|
||||
@@ -235,27 +234,14 @@ describe("authOptions", () => {
|
||||
|
||||
await credentialsProvider.options.authorize(credentials, {});
|
||||
|
||||
expect(applyIPRateLimit).toHaveBeenCalledWith(rateLimitConfigs.auth.login);
|
||||
expect(applyIPRateLimit).toHaveBeenCalledBefore(prisma.user.findUnique as any);
|
||||
expect(applyIPRateLimit).not.toHaveBeenCalled();
|
||||
expect(prisma.user.findUnique).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should block login when rate limit exceeded", async () => {
|
||||
test("should ignore app limiter errors because login is Envoy-managed", async () => {
|
||||
vi.mocked(applyIPRateLimit).mockRejectedValue(
|
||||
new Error("Maximum number of requests reached. Please try again later.")
|
||||
);
|
||||
const findUniqueSpy = vi.spyOn(prisma.user, "findUnique");
|
||||
|
||||
const credentials = { email: mockUser.email, password: mockPassword };
|
||||
|
||||
await expect(credentialsProvider.options.authorize(credentials, {})).rejects.toThrow(
|
||||
"Maximum number of requests reached. Please try again later."
|
||||
);
|
||||
|
||||
expect(findUniqueSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should use correct rate limit configuration", async () => {
|
||||
vi.mocked(applyIPRateLimit).mockResolvedValue({ allowed: true });
|
||||
vi.spyOn(prisma.user, "findUnique").mockResolvedValue({
|
||||
id: mockUserId,
|
||||
email: mockUser.email,
|
||||
@@ -266,13 +252,14 @@ describe("authOptions", () => {
|
||||
|
||||
const credentials = { email: mockUser.email, password: mockPassword };
|
||||
|
||||
await credentialsProvider.options.authorize(credentials, {});
|
||||
const result = await credentialsProvider.options.authorize(credentials, {});
|
||||
|
||||
expect(applyIPRateLimit).toHaveBeenCalledWith({
|
||||
interval: 900,
|
||||
allowedPerInterval: 30,
|
||||
namespace: "auth:login",
|
||||
expect(result).toEqual({
|
||||
id: mockUserId,
|
||||
email: mockUser.email,
|
||||
emailVerified: expect.any(Date),
|
||||
});
|
||||
expect(applyIPRateLimit).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -315,30 +302,28 @@ describe("authOptions", () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe("Rate Limiting", () => {
|
||||
test("should apply rate limiting before token verification", async () => {
|
||||
describe("Envoy-managed callback behavior", () => {
|
||||
test("should not apply in-app rate limiting before token verification", async () => {
|
||||
vi.mocked(applyIPRateLimit).mockResolvedValue({ allowed: true });
|
||||
|
||||
const credentials = { token: "sometoken" };
|
||||
|
||||
await expect(tokenProvider.options.authorize(credentials, {})).rejects.toThrow();
|
||||
|
||||
expect(applyIPRateLimit).toHaveBeenCalledWith(rateLimitConfigs.auth.verifyEmail);
|
||||
expect(applyIPRateLimit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should block verification when rate limit exceeded", async () => {
|
||||
test("should ignore app limiter errors because token verification is Envoy-managed", async () => {
|
||||
vi.mocked(applyIPRateLimit).mockRejectedValue(
|
||||
new Error("Maximum number of requests reached. Please try again later.")
|
||||
);
|
||||
const findUniqueSpy = vi.spyOn(prisma.user, "findUnique");
|
||||
|
||||
const credentials = { token: "sometoken" };
|
||||
|
||||
await expect(tokenProvider.options.authorize(credentials, {})).rejects.toThrow(
|
||||
"Maximum number of requests reached. Please try again later."
|
||||
"Either a user does not match the provided token or the token is invalid"
|
||||
);
|
||||
|
||||
expect(findUniqueSpy).not.toHaveBeenCalled();
|
||||
expect(applyIPRateLimit).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,8 +29,6 @@ import {
|
||||
shouldLogAuthFailure,
|
||||
verifyPassword,
|
||||
} from "@/modules/auth/lib/utils";
|
||||
import { applyIPRateLimit } from "@/modules/core/rate-limit/helpers";
|
||||
import { rateLimitConfigs } from "@/modules/core/rate-limit/rate-limit-configs";
|
||||
import { UNKNOWN_DATA } from "@/modules/ee/audit-logs/types/audit-log";
|
||||
import { getSSOProviders } from "@/modules/ee/sso/lib/providers";
|
||||
import { handleSsoCallback } from "@/modules/ee/sso/lib/sso-handlers";
|
||||
@@ -62,8 +60,6 @@ export const authOptions: NextAuthOptions = {
|
||||
backupCode: { label: "Backup Code", type: "input", placeholder: "Two-factor backup code" },
|
||||
},
|
||||
async authorize(credentials, _req) {
|
||||
await applyIPRateLimit(rateLimitConfigs.auth.login);
|
||||
|
||||
// Use email for rate limiting when available, fall back to "unknown_user" for credential validation
|
||||
const identifier = credentials?.email || "unknown_user"; // NOSONAR // We want to check for empty strings
|
||||
|
||||
@@ -252,8 +248,6 @@ export const authOptions: NextAuthOptions = {
|
||||
},
|
||||
},
|
||||
async authorize(credentials, _req) {
|
||||
await applyIPRateLimit(rateLimitConfigs.auth.verifyEmail);
|
||||
|
||||
// For token verification, we can't rate limit effectively by token (single-use)
|
||||
// So we use a generic identifier for token abuse attempts
|
||||
const identifier = "email_verification_attempts";
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { isRouteRateLimitedByEnvoy } from "./envoy-rate-limit-coverage";
|
||||
|
||||
describe("isRouteRateLimitedByEnvoy", () => {
|
||||
test("matches covered auth callback routes", () => {
|
||||
expect(
|
||||
isRouteRateLimitedByEnvoy({
|
||||
pathname: "/api/auth/callback/credentials",
|
||||
method: "POST",
|
||||
authType: "none",
|
||||
})
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isRouteRateLimitedByEnvoy({
|
||||
pathname: "/api/auth/callback/token",
|
||||
method: "POST",
|
||||
authType: "none",
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("matches covered api-key management and webhook routes", () => {
|
||||
expect(
|
||||
isRouteRateLimitedByEnvoy({
|
||||
pathname: "/api/v1/management/surveys",
|
||||
method: "GET",
|
||||
authType: "apiKey",
|
||||
})
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isRouteRateLimitedByEnvoy({
|
||||
pathname: "/api/v1/management/storage",
|
||||
method: "POST",
|
||||
authType: "apiKey",
|
||||
})
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isRouteRateLimitedByEnvoy({
|
||||
pathname: "/api/v1/webhooks/webhook-id",
|
||||
method: "DELETE",
|
||||
authType: "apiKey",
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("matches covered client routes", () => {
|
||||
expect(
|
||||
isRouteRateLimitedByEnvoy({
|
||||
pathname: "/api/v1/client/env_123/environment",
|
||||
method: "GET",
|
||||
authType: "none",
|
||||
})
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isRouteRateLimitedByEnvoy({
|
||||
pathname: "/api/v1/client/env_123/responses",
|
||||
method: "POST",
|
||||
authType: "none",
|
||||
})
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isRouteRateLimitedByEnvoy({
|
||||
pathname: "/api/v1/client/env_123/responses/response_123",
|
||||
method: "PUT",
|
||||
authType: "none",
|
||||
})
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isRouteRateLimitedByEnvoy({
|
||||
pathname: "/api/v1/client/env_123/displays",
|
||||
method: "POST",
|
||||
authType: "none",
|
||||
})
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isRouteRateLimitedByEnvoy({
|
||||
pathname: "/api/v1/client/env_123/user",
|
||||
method: "POST",
|
||||
authType: "none",
|
||||
})
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isRouteRateLimitedByEnvoy({
|
||||
pathname: "/api/v1/client/env_123/storage",
|
||||
method: "POST",
|
||||
authType: "none",
|
||||
})
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isRouteRateLimitedByEnvoy({
|
||||
pathname: "/api/v2/client/env_123/responses",
|
||||
method: "POST",
|
||||
authType: "none",
|
||||
})
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isRouteRateLimitedByEnvoy({
|
||||
pathname: "/api/v2/client/env_123/responses/response_123",
|
||||
method: "PUT",
|
||||
authType: "none",
|
||||
})
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isRouteRateLimitedByEnvoy({
|
||||
pathname: "/api/v2/client/env_123/displays",
|
||||
method: "POST",
|
||||
authType: "none",
|
||||
})
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isRouteRateLimitedByEnvoy({
|
||||
pathname: "/api/v2/client/env_123/storage",
|
||||
method: "POST",
|
||||
authType: "none",
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("matches covered api-key storage delete route", () => {
|
||||
expect(
|
||||
isRouteRateLimitedByEnvoy({
|
||||
pathname: "/storage/env_123/private/file.pdf",
|
||||
method: "DELETE",
|
||||
authType: "apiKey",
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("does not match excluded or uncovered routes", () => {
|
||||
expect(
|
||||
isRouteRateLimitedByEnvoy({
|
||||
pathname: "/api/v1/client/og",
|
||||
method: "GET",
|
||||
authType: "none",
|
||||
})
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
isRouteRateLimitedByEnvoy({
|
||||
pathname: "/api/v2/health",
|
||||
method: "GET",
|
||||
authType: "none",
|
||||
})
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
isRouteRateLimitedByEnvoy({
|
||||
pathname: "/api/v1/client/env_123/environment",
|
||||
method: "PATCH",
|
||||
authType: "none",
|
||||
})
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
isRouteRateLimitedByEnvoy({
|
||||
pathname: "/api/v1/client/env_123/displays",
|
||||
method: "GET",
|
||||
authType: "none",
|
||||
})
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
isRouteRateLimitedByEnvoy({
|
||||
pathname: "/api/v2/client/env_123/responses",
|
||||
method: "PUT",
|
||||
authType: "none",
|
||||
})
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
isRouteRateLimitedByEnvoy({
|
||||
pathname: "/api/v2/client/env_123/responses/response_123",
|
||||
method: "POST",
|
||||
authType: "none",
|
||||
})
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
isRouteRateLimitedByEnvoy({
|
||||
pathname: "/api/v2/client/env_123/environment",
|
||||
method: "GET",
|
||||
authType: "none",
|
||||
})
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
isRouteRateLimitedByEnvoy({
|
||||
pathname: "/api/v2/client/env_123/user",
|
||||
method: "POST",
|
||||
authType: "none",
|
||||
})
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
isRouteRateLimitedByEnvoy({
|
||||
pathname: "/api/v1/management/me",
|
||||
method: "GET",
|
||||
authType: "session",
|
||||
})
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
isRouteRateLimitedByEnvoy({
|
||||
pathname: "/api/v1/management/storage",
|
||||
method: "POST",
|
||||
authType: "session",
|
||||
})
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
isRouteRateLimitedByEnvoy({
|
||||
pathname: "/storage/env_123/private/file.pdf",
|
||||
method: "DELETE",
|
||||
authType: "session",
|
||||
})
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
isRouteRateLimitedByEnvoy({
|
||||
pathname: "/api/v1/webhooks",
|
||||
method: "GET",
|
||||
authType: "apiKey",
|
||||
})
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
isRouteRateLimitedByEnvoy({
|
||||
pathname: "/api/v1/client/env_123/environment",
|
||||
method: "OPTIONS",
|
||||
authType: "none",
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
export type TEnvoyRateLimitAuthType = "none" | "apiKey" | "session";
|
||||
|
||||
type TEnvoyRateLimitRequest = {
|
||||
pathname: string;
|
||||
method: string;
|
||||
authType: TEnvoyRateLimitAuthType;
|
||||
};
|
||||
|
||||
const V1_CLIENT_STORAGE_PATTERN = /^\/api\/v1\/client\/[^/]+\/storage$/;
|
||||
const V1_CLIENT_ENVIRONMENT_PATTERN = /^\/api\/v1\/client\/[^/]+\/environment$/;
|
||||
const V1_CLIENT_RESPONSES_PATTERN = /^\/api\/v1\/client\/[^/]+\/responses$/;
|
||||
const V1_CLIENT_RESPONSE_PATTERN = /^\/api\/v1\/client\/[^/]+\/responses\/[^/]+$/;
|
||||
const V1_CLIENT_DISPLAYS_PATTERN = /^\/api\/v1\/client\/[^/]+\/displays$/;
|
||||
const V1_CLIENT_USER_PATTERN = /^\/api\/v1\/client\/[^/]+\/user$/;
|
||||
const V2_CLIENT_RESPONSES_PATTERN = /^\/api\/v2\/client\/[^/]+\/responses$/;
|
||||
const V2_CLIENT_RESPONSE_PATTERN = /^\/api\/v2\/client\/[^/]+\/responses\/[^/]+$/;
|
||||
const V2_CLIENT_DISPLAYS_PATTERN = /^\/api\/v2\/client\/[^/]+\/displays$/;
|
||||
const V2_CLIENT_STORAGE_PATTERN = /^\/api\/v2\/client\/[^/]+\/storage$/;
|
||||
const STORAGE_DELETE_PATTERN = /^\/storage\/[^/]+\/(public|private)\/.+$/;
|
||||
|
||||
const V1_MANAGEMENT_PREFIX = "/api/v1/management/";
|
||||
const V1_WEBHOOKS_PREFIX = "/api/v1/webhooks/";
|
||||
|
||||
const V1_GENERAL_METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
|
||||
const normalizeMethod = (method: string): string => method.toUpperCase();
|
||||
|
||||
const matchesPrefixedPath = (pathname: string, prefix: string): boolean => pathname.startsWith(prefix);
|
||||
|
||||
/**
|
||||
* Mirrors the live Envoy rate-limit policy set.
|
||||
* Keep this matcher aligned with the Gateway policies when coverage changes.
|
||||
*/
|
||||
export const isRouteRateLimitedByEnvoy = ({
|
||||
pathname,
|
||||
method,
|
||||
authType,
|
||||
}: TEnvoyRateLimitRequest): boolean => {
|
||||
const normalizedMethod = normalizeMethod(method);
|
||||
|
||||
if (normalizedMethod === "OPTIONS") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (authType === "none" && normalizedMethod === "POST" && pathname === "/api/auth/callback/credentials") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (authType === "none" && normalizedMethod === "POST" && pathname === "/api/auth/callback/token") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
authType === "apiKey" &&
|
||||
V1_GENERAL_METHODS.has(normalizedMethod) &&
|
||||
matchesPrefixedPath(pathname, V1_MANAGEMENT_PREFIX)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
authType === "apiKey" &&
|
||||
V1_GENERAL_METHODS.has(normalizedMethod) &&
|
||||
matchesPrefixedPath(pathname, V1_WEBHOOKS_PREFIX)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (authType === "apiKey" && normalizedMethod === "DELETE" && STORAGE_DELETE_PATTERN.test(pathname)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (authType !== "none") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (normalizedMethod === "POST" && V1_CLIENT_STORAGE_PATTERN.test(pathname)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (normalizedMethod === "GET" && V1_CLIENT_ENVIRONMENT_PATTERN.test(pathname)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (normalizedMethod === "POST" && V1_CLIENT_RESPONSES_PATTERN.test(pathname)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (normalizedMethod === "PUT" && V1_CLIENT_RESPONSE_PATTERN.test(pathname)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (normalizedMethod === "POST" && V1_CLIENT_DISPLAYS_PATTERN.test(pathname)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (normalizedMethod === "POST" && V1_CLIENT_USER_PATTERN.test(pathname)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (normalizedMethod === "POST" && V2_CLIENT_RESPONSES_PATTERN.test(pathname)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (normalizedMethod === "PUT" && V2_CLIENT_RESPONSE_PATTERN.test(pathname)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (normalizedMethod === "POST" && V2_CLIENT_DISPLAYS_PATTERN.test(pathname)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (normalizedMethod === "POST" && V2_CLIENT_STORAGE_PATTERN.test(pathname)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
@@ -0,0 +1,312 @@
|
||||
# Environment Reference Audit
|
||||
|
||||
Inventory of references to `environmentId` and `environment`/`environments` across product and docs code.
|
||||
|
||||
## Scope
|
||||
|
||||
- `apps/web`
|
||||
- `packages`
|
||||
- `docs`
|
||||
- `apps/storybook`
|
||||
- `openapi.yml`
|
||||
|
||||
## Totals
|
||||
|
||||
- Files with matches: **290**
|
||||
- Total `environmentId` matches: **994**
|
||||
- Total `environment`/`environments` matches: **997**
|
||||
|
||||
## File-Level Inventory
|
||||
|
||||
| Area | File | environmentId Count | environment/environments Count | Suggested Handling |
|
||||
| --- | --- | ---: | ---: | --- |
|
||||
| Product | `apps/storybook/src/stories/Configure.mdx` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/lib/surveys.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `apps/web/app/(app)/workspaces/[workspaceId]/components/WorkspaceStorageHandler.tsx` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/utils.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/app/(redirects)/environments/[environmentId]/[...path]/route.ts` | 4 | 0 | Keep route for backward compatibility, but label as legacy/deprecated and redirect to workspace URL shape. |
|
||||
| Product | `apps/web/app/(redirects)/environments/[environmentId]/route.ts` | 4 | 0 | Keep route for backward compatibility, but label as legacy/deprecated and redirect to workspace URL shape. |
|
||||
| Product | `apps/web/app/api/(internal)/pipeline/lib/telemetry.ts` | 0 | 2 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/app/api/v1/client/[workspaceId]/displays/route.ts` | 1 | 0 | Use workspaceId as canonical input; retain environmentId only as backward-compatible alias and mark deprecation in comments/docs. |
|
||||
| Product | `apps/web/app/api/v1/client/[workspaceId]/environment/lib/data.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `apps/web/app/api/v1/client/[workspaceId]/environment/lib/data.ts` | 0 | 4 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/app/api/v1/client/[workspaceId]/environment/lib/environmentState.test.ts` | 1 | 1 | Rename test variable names/fixtures to workspaceId while preserving compatibility coverage for legacy environmentId inputs. |
|
||||
| Product | `apps/web/app/api/v1/client/[workspaceId]/environment/lib/environmentState.ts` | 0 | 2 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/app/api/v1/client/[workspaceId]/environment/route.ts` | 1 | 4 | Use workspaceId as canonical input; retain environmentId only as backward-compatible alias and mark deprecation in comments/docs. |
|
||||
| Product | `apps/web/app/api/v1/client/[workspaceId]/responses/route.ts` | 1 | 0 | Use workspaceId as canonical input; retain environmentId only as backward-compatible alias and mark deprecation in comments/docs. |
|
||||
| Product | `apps/web/app/api/v1/client/[workspaceId]/storage/route.ts` | 1 | 1 | Use workspaceId as canonical input; retain environmentId only as backward-compatible alias and mark deprecation in comments/docs. |
|
||||
| Product | `apps/web/app/api/v1/management/action-classes/[actionClassId]/route.ts` | 1 | 1 | Use workspaceId as canonical input; retain environmentId only as backward-compatible alias and mark deprecation in comments/docs. |
|
||||
| Product | `apps/web/app/api/v1/management/lib/workspace-resolver.ts` | 3 | 1 | Use workspaceId as canonical input; retain environmentId only as backward-compatible alias and mark deprecation in comments/docs. |
|
||||
| Product | `apps/web/app/api/v1/management/me/route.ts` | 0 | 3 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/app/api/v1/management/responses/route.ts` | 1 | 1 | Use workspaceId as canonical input; retain environmentId only as backward-compatible alias and mark deprecation in comments/docs. |
|
||||
| Product | `apps/web/app/api/v1/management/surveys/route.ts` | 1 | 1 | Use workspaceId as canonical input; retain environmentId only as backward-compatible alias and mark deprecation in comments/docs. |
|
||||
| Product | `apps/web/app/api/v1/webhooks/route.ts` | 1 | 0 | Use workspaceId as canonical input; retain environmentId only as backward-compatible alias and mark deprecation in comments/docs. |
|
||||
| Product | `apps/web/app/api/v2/client/[environmentId]/environment/route.test.ts` | 0 | 9 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `apps/web/app/api/v2/client/[workspaceId]/displays/route.test.ts` | 8 | 0 | Rename test variable names/fixtures to workspaceId while preserving compatibility coverage for legacy environmentId inputs. |
|
||||
| Product | `apps/web/app/api/v2/client/[workspaceId]/displays/route.ts` | 1 | 0 | Use workspaceId as canonical input; retain environmentId only as backward-compatible alias and mark deprecation in comments/docs. |
|
||||
| Product | `apps/web/app/api/v2/client/[workspaceId]/environment/route.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/app/api/v2/client/[workspaceId]/responses/route.test.ts` | 7 | 0 | Rename test variable names/fixtures to workspaceId while preserving compatibility coverage for legacy environmentId inputs. |
|
||||
| Product | `apps/web/app/api/v2/client/[workspaceId]/responses/route.ts` | 1 | 0 | Use workspaceId as canonical input; retain environmentId only as backward-compatible alias and mark deprecation in comments/docs. |
|
||||
| Product | `apps/web/app/api/v3/lib/workspace-context.test.ts` | 1 | 0 | Rename test variable names/fixtures to workspaceId while preserving compatibility coverage for legacy environmentId inputs. |
|
||||
| Product | `apps/web/app/api/v3/lib/workspace-context.ts` | 1 | 0 | Use workspaceId as canonical input; retain environmentId only as backward-compatible alias and mark deprecation in comments/docs. |
|
||||
| Product | `apps/web/app/lib/api/api-backwards-compat.ts` | 0 | 2 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/app/lib/api/api-error-reporter.test.ts` | 0 | 4 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `apps/web/app/lib/api/parse-and-validate-json-body.test.ts` | 3 | 0 | Rename test variable names/fixtures to workspaceId while preserving compatibility coverage for legacy environmentId inputs. |
|
||||
| Product | `apps/web/app/lib/api/with-api-logging.test.ts` | 0 | 4 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `apps/web/app/middleware/domain-utils.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/app/middleware/endpoint-validator.test.ts` | 0 | 18 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `apps/web/app/middleware/endpoint-validator.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/app/middleware/route-config.ts` | 1 | 0 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
|
||||
| Product | `apps/web/app/sentry/SentryProvider.tsx` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/app/storage/[workspaceId]/[accessType]/[fileName]/route.ts` | 3 | 0 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
|
||||
| Product | `apps/web/i18n.lock` | 1 | 1 | Update user-facing translations from Environment -> Workspace (or final replacement term); keep temporary key aliases if needed. |
|
||||
| Product | `apps/web/instrumentation-node.ts` | 0 | 9 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/lib/actionClass/service.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `apps/web/lib/cache/index.test.ts` | 0 | 2 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `apps/web/lib/constants.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/lib/env.test.ts` | 0 | 3 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `apps/web/lib/env.ts` | 0 | 5 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/lib/integration/service.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `apps/web/lib/jwt.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `apps/web/lib/localStorage.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/lib/organization/service.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `apps/web/lib/survey/service.test.ts` | 0 | 3 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `apps/web/lib/utils/resolve-client-id.ts` | 1 | 0 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
|
||||
| Product | `apps/web/lib/utils/services.ts` | 1 | 0 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
|
||||
| Product | `apps/web/locales/de-DE.json` | 1 | 1 | Update user-facing translations from Environment -> Workspace (or final replacement term); keep temporary key aliases if needed. |
|
||||
| Product | `apps/web/locales/en-US.json` | 1 | 12 | Update user-facing translations from Environment -> Workspace (or final replacement term); keep temporary key aliases if needed. |
|
||||
| Product | `apps/web/locales/es-ES.json` | 1 | 1 | Update user-facing translations from Environment -> Workspace (or final replacement term); keep temporary key aliases if needed. |
|
||||
| Product | `apps/web/locales/fr-FR.json` | 1 | 1 | Update user-facing translations from Environment -> Workspace (or final replacement term); keep temporary key aliases if needed. |
|
||||
| Product | `apps/web/locales/hu-HU.json` | 1 | 1 | Update user-facing translations from Environment -> Workspace (or final replacement term); keep temporary key aliases if needed. |
|
||||
| Product | `apps/web/locales/ja-JP.json` | 1 | 1 | Update user-facing translations from Environment -> Workspace (or final replacement term); keep temporary key aliases if needed. |
|
||||
| Product | `apps/web/locales/nl-NL.json` | 1 | 1 | Update user-facing translations from Environment -> Workspace (or final replacement term); keep temporary key aliases if needed. |
|
||||
| Product | `apps/web/locales/pt-BR.json` | 1 | 1 | Update user-facing translations from Environment -> Workspace (or final replacement term); keep temporary key aliases if needed. |
|
||||
| Product | `apps/web/locales/pt-PT.json` | 1 | 1 | Update user-facing translations from Environment -> Workspace (or final replacement term); keep temporary key aliases if needed. |
|
||||
| Product | `apps/web/locales/ro-RO.json` | 1 | 1 | Update user-facing translations from Environment -> Workspace (or final replacement term); keep temporary key aliases if needed. |
|
||||
| Product | `apps/web/locales/ru-RU.json` | 1 | 1 | Update user-facing translations from Environment -> Workspace (or final replacement term); keep temporary key aliases if needed. |
|
||||
| Product | `apps/web/locales/sv-SE.json` | 1 | 1 | Update user-facing translations from Environment -> Workspace (or final replacement term); keep temporary key aliases if needed. |
|
||||
| Product | `apps/web/locales/zh-Hans-CN.json` | 1 | 1 | Update user-facing translations from Environment -> Workspace (or final replacement term); keep temporary key aliases if needed. |
|
||||
| Product | `apps/web/locales/zh-Hant-TW.json` | 1 | 1 | Update user-facing translations from Environment -> Workspace (or final replacement term); keep temporary key aliases if needed. |
|
||||
| Product | `apps/web/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/route.ts` | 0 | 3 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/modules/api/v2/management/lib/workspace-resolver.ts` | 3 | 1 | Use workspaceId as canonical input; retain environmentId only as backward-compatible alias and mark deprecation in comments/docs. |
|
||||
| Product | `apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/route.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/contact.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/tests/contact.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `apps/web/modules/api/v2/management/surveys/types/surveys.ts` | 1 | 0 | Use workspaceId as canonical input; retain environmentId only as backward-compatible alias and mark deprecation in comments/docs. |
|
||||
| Product | `apps/web/modules/auth/lib/verification-links.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `apps/web/modules/core/rate-limit/envoy-rate-limit-coverage.test.ts` | 0 | 4 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `apps/web/modules/core/rate-limit/envoy-rate-limit-coverage.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/modules/core/rate-limit/rate-limit-load.test.ts` | 0 | 3 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `apps/web/modules/core/rate-limit/rate-limit.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/modules/ee/contacts/api/v1/management/contact-attribute-keys/route.ts` | 1 | 1 | Use workspaceId as canonical input; retain environmentId only as backward-compatible alias and mark deprecation in comments/docs. |
|
||||
| Product | `apps/web/modules/ee/contacts/api/v2/management/contacts/bulk/lib/openapi.ts` | 2 | 0 | Update schema/examples to prefer workspaceId; keep environmentId marked as deprecated alias where compatibility is required. |
|
||||
| Product | `apps/web/modules/ee/contacts/api/v2/management/contacts/bulk/route.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/modules/ee/contacts/api/v2/management/contacts/lib/openapi.ts` | 2 | 3 | Update schema/examples to prefer workspaceId; keep environmentId marked as deprecated alias where compatibility is required. |
|
||||
| Product | `apps/web/modules/ee/contacts/api/v2/management/contacts/route.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/modules/ee/contacts/lib/attributes.ts` | 0 | 2 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/modules/ee/license-check/lib/license.test.ts` | 0 | 16 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `apps/web/modules/ee/license-check/lib/license.ts` | 0 | 2 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/modules/ee/sso/lib/providers.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `apps/web/modules/ee/sso/lib/tests/sso-handlers.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `apps/web/modules/organization/settings/api-keys/lib/api-keys.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `apps/web/modules/storage/service.test.ts` | 1 | 0 | Rename test variable names/fixtures to workspaceId while preserving compatibility coverage for legacy environmentId inputs. |
|
||||
| Product | `apps/web/modules/storage/service.ts` | 3 | 0 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
|
||||
| Product | `apps/web/modules/survey/components/template-list/lib/survey.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/modules/survey/editor/components/edit-welcome-card.tsx` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/modules/survey/editor/components/survey-editor.tsx` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/modules/survey/editor/lib/action-utils.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `apps/web/modules/survey/editor/lib/workspace.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `apps/web/modules/survey/link/contact-survey/page.tsx` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/modules/survey/link/page.tsx` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/modules/survey/list/types/surveys.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/modules/survey/templates/components/template-container.tsx` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/modules/ui/components/delete-dialog/stories.tsx` | 0 | 3 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/modules/ui/components/preview-survey/index.tsx` | 0 | 5 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/modules/utils/hooks/useGetBillingInfo.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `apps/web/modules/workspaces/lib/utils.ts` | 0 | 2 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/modules/workspaces/settings/(setup)/app-connection/loading.tsx` | 1 | 0 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
|
||||
| Product | `apps/web/modules/workspaces/settings/(setup)/app-connection/page.tsx` | 1 | 0 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
|
||||
| Product | `apps/web/modules/workspaces/settings/(setup)/components/ActionActivityTab.tsx` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/modules/workspaces/settings/general/actions.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/next.config.mjs` | 6 | 7 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
|
||||
| Product | `apps/web/playwright/api/auth/security.spec.ts` | 0 | 6 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `apps/web/playwright/js.spec.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `apps/web/playwright/lib/utils.ts` | 0 | 2 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/playwright/survey.spec.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `apps/web/proxy.test.ts` | 0 | 3 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `apps/web/scripts/docker/read-secrets.sh` | 0 | 2 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/scripts/openapi/merge-client-endpoints.ts` | 10 | 10 | Update schema/examples to prefer workspaceId; keep environmentId marked as deprecated alias where compatibility is required. |
|
||||
| Product | `apps/web/sentry.edge.config.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/sentry.server.config.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/tsconfig.tsbuildinfo` | 483 | 151 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
|
||||
| Product | `apps/web/vite.config.mts` | 0 | 2 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `apps/web/vitestSetup.ts` | 0 | 2 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Docs | `docs/api-reference/client-api--display/create-display.mdx` | 1 | 0 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
|
||||
| Docs | `docs/api-reference/client-api--display/update-display.mdx` | 1 | 0 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
|
||||
| Docs | `docs/api-reference/client-api--people/create-person.mdx` | 1 | 0 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
|
||||
| Docs | `docs/api-reference/client-api--people/update-person.mdx` | 1 | 0 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
|
||||
| Docs | `docs/api-reference/client-api--response/create-response.mdx` | 1 | 0 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
|
||||
| Docs | `docs/api-reference/client-api--response/update-response.mdx` | 1 | 0 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
|
||||
| Docs | `docs/api-reference/generate-key.mdx` | 0 | 2 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/api-reference/openapi.json` | 85 | 36 | Update schema/examples to prefer workspaceId; keep environmentId marked as deprecated alias where compatibility is required. |
|
||||
| Docs | `docs/api-v2-reference/introduction.mdx` | 1 | 0 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
|
||||
| Docs | `docs/api-v2-reference/openapi.yml` | 52 | 35 | Update schema/examples to prefer workspaceId; keep environmentId marked as deprecated alias where compatibility is required. |
|
||||
| Docs | `docs/api-v3-reference/openapi.yml` | 1 | 8 | Update schema/examples to prefer workspaceId; keep environmentId marked as deprecated alias where compatibility is required. |
|
||||
| Docs | `docs/development/contribution/contribution.mdx` | 0 | 2 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/development/local-setup/github-codespaces.mdx` | 0 | 2 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/development/standards/organization/file-and-directory-organization.mdx` | 0 | 3 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/development/standards/organization/naming-conventions.mdx` | 1 | 0 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
|
||||
| Docs | `docs/development/standards/practices/error-handling.mdx` | 1 | 0 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
|
||||
| Docs | `docs/development/standards/technical/language-specific-conventions.mdx` | 1 | 0 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
|
||||
| Docs | `docs/development/technical-handbook/background-job-processing.mdx` | 3 | 1 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
|
||||
| Docs | `docs/development/technical-handbook/database-model.mdx` | 0 | 14 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/development/technical-handbook/tenant-separation.mdx` | 0 | 14 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/docs.json` | 0 | 2 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/overview/open-source.mdx` | 0 | 1 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/self-hosting/advanced/enterprise-features/audit-logging.mdx` | 1 | 1 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
|
||||
| Docs | `docs/self-hosting/advanced/license-activation.mdx` | 0 | 3 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/self-hosting/advanced/migration.mdx` | 3 | 38 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
|
||||
| Docs | `docs/self-hosting/advanced/rate-limiting.mdx` | 15 | 2 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
|
||||
| Docs | `docs/self-hosting/auth-behavior.mdx` | 0 | 6 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/self-hosting/configuration/auth-sso/azure-ad-oauth.mdx` | 0 | 3 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/self-hosting/configuration/auth-sso/google-oauth.mdx` | 0 | 5 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/self-hosting/configuration/auth-sso/keycloak-oidc.mdx` | 0 | 4 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/self-hosting/configuration/auth-sso/open-id-connect.mdx` | 0 | 2 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/self-hosting/configuration/auth-sso/saml-sso.mdx` | 0 | 1 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/self-hosting/configuration/cdn.mdx` | 1 | 0 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
|
||||
| Docs | `docs/self-hosting/configuration/custom-ssl.mdx` | 0 | 1 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/self-hosting/configuration/custom-subpath.mdx` | 0 | 4 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/self-hosting/configuration/domain-configuration.mdx` | 5 | 2 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
|
||||
| Docs | `docs/self-hosting/configuration/environment-variables.mdx` | 0 | 3 | No entity rename: this is deployment env-var documentation, not the deprecated product Environment entity. |
|
||||
| Docs | `docs/self-hosting/configuration/file-uploads.mdx` | 0 | 5 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/self-hosting/configuration/integrations/airtable.mdx` | 0 | 3 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/self-hosting/configuration/integrations/google-sheets.mdx` | 0 | 3 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/self-hosting/configuration/integrations/n8n.mdx` | 0 | 3 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/self-hosting/configuration/integrations/notion.mdx` | 0 | 3 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/self-hosting/configuration/integrations/slack.mdx` | 0 | 3 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/self-hosting/configuration/smtp.mdx` | 0 | 6 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/self-hosting/setup/cluster-setup.mdx` | 0 | 4 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/self-hosting/setup/docker.mdx` | 0 | 6 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/self-hosting/setup/kubernetes.mdx` | 0 | 2 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/self-hosting/setup/monitoring.mdx` | 0 | 7 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/self-hosting/setup/one-click.mdx` | 0 | 8 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/xm-and-surveys/core-features/integrations/n8n.mdx` | 0 | 1 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/xm-and-surveys/core-features/integrations/notion.mdx` | 0 | 3 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/xm-and-surveys/core-features/integrations/slack.mdx` | 0 | 4 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/xm-and-surveys/core-features/integrations/webhooks.mdx` | 0 | 5 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/xm-and-surveys/core-features/integrations/wordpress.mdx` | 4 | 2 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
|
||||
| Docs | `docs/xm-and-surveys/core-features/integrations/zapier.mdx` | 0 | 1 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/xm-and-surveys/core-features/test-environment.mdx` | 0 | 19 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/xm-and-surveys/surveys/general-features/multi-language-surveys.mdx` | 1 | 1 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
|
||||
| Docs | `docs/xm-and-surveys/surveys/general-features/recall.mdx` | 0 | 1 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/xm-and-surveys/surveys/general-features/spam-protection.mdx` | 0 | 3 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/xm-and-surveys/surveys/general-features/tags.mdx` | 0 | 6 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/xm-and-surveys/surveys/website-app-surveys/actions.mdx` | 0 | 1 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/xm-and-surveys/surveys/website-app-surveys/framework-guides.mdx` | 9 | 24 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
|
||||
| Docs | `docs/xm-and-surveys/surveys/website-app-surveys/google-tag-manager.mdx` | 3 | 3 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
|
||||
| Docs | `docs/xm-and-surveys/surveys/website-app-surveys/quickstart.mdx` | 0 | 1 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/xm-and-surveys/surveys/website-app-surveys/user-identification.mdx` | 0 | 1 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/xm-and-surveys/xm/best-practices/docs-feedback.mdx` | 0 | 10 | Review each mention and rename product-entity Environment wording; keep deployment/runtime environment wording unchanged. |
|
||||
| Docs | `docs/xm-and-surveys/xm/best-practices/headless-surveys.mdx` | 3 | 6 | Rewrite docs examples to workspaceId and explicitly mention legacy environmentId compatibility window. |
|
||||
| Product | `openapi.yml` | 4 | 0 | Update schema/examples to prefer workspaceId; keep environmentId marked as deprecated alias where compatibility is required. |
|
||||
| Product | `packages/ai/src/provider.test.ts` | 0 | 5 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `packages/ai/src/provider.ts` | 0 | 15 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `packages/ai/src/providers/aws.ts` | 0 | 11 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `packages/ai/src/providers/azure.ts` | 0 | 13 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `packages/ai/src/providers/gcp.ts` | 0 | 15 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `packages/ai/src/registry.ts` | 0 | 3 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `packages/ai/src/shared.ts` | 0 | 2 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `packages/ai/src/text.test.ts` | 0 | 3 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `packages/ai/src/text.ts` | 0 | 2 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `packages/ai/vite.config.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `packages/cache/.cursor/rules/cache-package.md` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `packages/cache/src/cache-integration.test.ts` | 0 | 2 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `packages/cache/src/cache-keys.test.ts` | 0 | 6 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `packages/cache/src/client.test.ts` | 0 | 2 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `packages/cache/src/client.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `packages/cache/types/error.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `packages/cache/vite.config.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `packages/database/README.md` | 0 | 5 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `packages/database/migration/20230329205933_init/migration.sql` | 10 | 6 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
|
||||
| Product | `packages/database/migration/20230405105937_add_api_keys_to_environments/migration.sql` | 3 | 1 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
|
||||
| Product | `packages/database/migration/20230505085230_add_webhooks/migration.sql` | 2 | 1 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
|
||||
| Product | `packages/database/migration/20230624161355_add_tags/migration.sql` | 3 | 1 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
|
||||
| Product | `packages/database/migration/20230915154251_add_integration/migration.sql` | 3 | 1 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
|
||||
| Product | `packages/database/migration/20231030105533_add_cascade_delete_to_integrations/migration.sql` | 1 | 1 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
|
||||
| Product | `packages/database/migration/20231107145619_add_indexes/migration.sql` | 7 | 1 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
|
||||
| Product | `packages/database/migration/20231109052945_restructure_session_action_person/migration.sql` | 2 | 0 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
|
||||
| Product | `packages/database/migration/20240207041922_advanced_targeting/migration.sql` | 4 | 1 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
|
||||
| Product | `packages/database/migration/20240229141200_add_attribute_class_indexes/migration.sql` | 2 | 0 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
|
||||
| Product | `packages/database/migration/20240327140901_add_more_indexes/migration.sql` | 2 | 0 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
|
||||
| Product | `packages/database/migration/20240501111944_refactors_actions_and_removes_inline_triggers/migration.sql` | 2 | 0 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
|
||||
| Product | `packages/database/migration/20240610055828_adds_app_and_website_status_indicators/migration.sql` | 0 | 1 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
|
||||
| Product | `packages/database/migration/20241004070040_removed_website_setup_completed/migration.sql` | 0 | 2 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
|
||||
| Product | `packages/database/migration/20241010133706_xm_user_identification/migration.sql` | 3 | 0 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
|
||||
| Product | `packages/database/migration/20241017124431_add_documents_and_insights/migration.sql` | 4 | 2 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
|
||||
| Product | `packages/database/migration/20241120150728_product_revamp/migration.sql` | 0 | 5 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
|
||||
| Product | `packages/database/migration/20241209104738_xm_user_identification/migration.ts` | 11 | 12 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
|
||||
| Product | `packages/database/migration/20241209111404_xm_attribute_removal/migration.ts` | 11 | 0 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
|
||||
| Product | `packages/database/migration/20250326083401_add_api_keys_to_organization/migration.sql` | 4 | 1 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
|
||||
| Product | `packages/database/migration/20250326111101_move_api_keys_to_api_keys_new/migration.ts` | 7 | 4 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
|
||||
| Product | `packages/database/migration/20250911192630_remove_deprecated_fields_and_tables/migration.sql` | 0 | 2 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
|
||||
| Product | `packages/database/migration/20260204124556_add_language_default_attribute_key/migration.ts` | 2 | 4 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
|
||||
| Product | `packages/database/migration/20260330000000_rename_project_to_workspace/migration.sql` | 0 | 3 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
|
||||
| Product | `packages/database/migration/20260401000000_add_workspace_id_to_environment_owned_models/migration.sql` | 0 | 3 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
|
||||
| Product | `packages/database/migration/20260401000001_promote_dev_environments/migration.ts` | 10 | 21 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
|
||||
| Product | `packages/database/migration/20260401000002_backfill_workspace_id/migration.ts` | 2 | 2 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
|
||||
| Product | `packages/database/migration/20260402000000_make_workspace_id_not_null/migration.sql` | 8 | 2 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
|
||||
| Product | `packages/database/migration/20260403000000_remove_environment_model/migration.sql` | 12 | 6 | No retrofit needed for historical migrations; keep as-is and use new terminology only in future migrations/docs. |
|
||||
| Product | `packages/database/migrations/20230329205933_init/migration.sql` | 10 | 6 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
|
||||
| Product | `packages/database/migrations/20230405105937_add_api_keys_to_environments/migration.sql` | 3 | 1 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
|
||||
| Product | `packages/database/migrations/20230505085230_add_webhooks/migration.sql` | 2 | 1 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
|
||||
| Product | `packages/database/migrations/20230624161355_add_tags/migration.sql` | 3 | 1 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
|
||||
| Product | `packages/database/migrations/20230915154251_add_integration/migration.sql` | 3 | 1 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
|
||||
| Product | `packages/database/migrations/20231030105533_add_cascade_delete_to_integrations/migration.sql` | 1 | 1 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
|
||||
| Product | `packages/database/migrations/20231107145619_add_indexes/migration.sql` | 7 | 1 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
|
||||
| Product | `packages/database/migrations/20231109052945_restructure_session_action_person/migration.sql` | 2 | 0 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
|
||||
| Product | `packages/database/migrations/20240207041922_advanced_targeting/migration.sql` | 4 | 1 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
|
||||
| Product | `packages/database/migrations/20240229141200_add_attribute_class_indexes/migration.sql` | 2 | 0 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
|
||||
| Product | `packages/database/migrations/20240327140901_add_more_indexes/migration.sql` | 2 | 0 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
|
||||
| Product | `packages/database/migrations/20240501111944_refactors_actions_and_removes_inline_triggers/migration.sql` | 2 | 0 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
|
||||
| Product | `packages/database/migrations/20240610055828_adds_app_and_website_status_indicators/migration.sql` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `packages/database/migrations/20241004070040_removed_website_setup_completed/migration.sql` | 0 | 2 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `packages/database/migrations/20241010133706_xm_user_identification/migration.sql` | 3 | 0 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
|
||||
| Product | `packages/database/migrations/20241017124431_add_documents_and_insights/migration.sql` | 4 | 2 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
|
||||
| Product | `packages/database/migrations/20241120150728_product_revamp/migration.sql` | 0 | 5 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `packages/database/migrations/20250326083401_add_api_keys_to_organization/migration.sql` | 4 | 1 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
|
||||
| Product | `packages/database/migrations/20250911192630_remove_deprecated_fields_and_tables/migration.sql` | 0 | 2 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `packages/database/migrations/20260330000000_rename_project_to_workspace/migration.sql` | 0 | 3 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `packages/database/migrations/20260401000000_add_workspace_id_to_environment_owned_models/migration.sql` | 0 | 3 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `packages/database/migrations/20260402000000_make_workspace_id_not_null/migration.sql` | 8 | 2 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
|
||||
| Product | `packages/database/migrations/20260403000000_remove_environment_model/migration.sql` | 12 | 6 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
|
||||
| Product | `packages/email/src/lib/mock-translate.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `packages/i18n-utils/vite.config.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `packages/js-core/README.md` | 0 | 3 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `packages/js-core/src/index.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `packages/js-core/src/lib/common/api.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `packages/js-core/src/lib/common/setup.ts` | 9 | 7 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
|
||||
| Product | `packages/js-core/src/lib/common/tests/api.test.ts` | 0 | 3 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `packages/js-core/src/lib/common/tests/setup.test.ts` | 18 | 0 | Rename test variable names/fixtures to workspaceId while preserving compatibility coverage for legacy environmentId inputs. |
|
||||
| Product | `packages/js-core/src/lib/common/tests/utils.test.ts` | 0 | 2 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `packages/js-core/src/lib/workspace/state.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `packages/js-core/src/lib/workspace/tests/state.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `packages/js-core/src/types/config.ts` | 3 | 1 | Refactor symbol/param names to workspaceId (or final replacement term) and keep compatibility aliases only at boundaries. |
|
||||
| Product | `packages/js-core/vite.config.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `packages/logger/src/logger.test.ts` | 0 | 4 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `packages/logger/src/logger.ts` | 0 | 4 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `packages/storage/.cursor/rules/storage-package.md` | 0 | 9 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `packages/storage/src/client.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `packages/storage/src/client.ts` | 0 | 4 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `packages/storage/src/constants.test.ts` | 0 | 7 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `packages/storage/src/service.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `packages/storage/vite.config.ts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `packages/survey-ui/vite.config.mts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `packages/surveys/README.md` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `packages/surveys/src/lib/html-utils.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `packages/surveys/src/lib/html-utils.ts` | 0 | 3 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `packages/surveys/src/lib/response.queue.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `packages/surveys/src/lib/styles.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `packages/surveys/src/lib/ttc.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `packages/surveys/src/lib/use-online-status.test.ts` | 0 | 1 | Review wording; keep runtime-environment mentions, but rename product-entity labels to workspace terminology. |
|
||||
| Product | `packages/surveys/vite.config.mts` | 0 | 1 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
| Product | `packages/types/common.ts` | 0 | 2 | Review mentions and replace product-entity Environment terminology with the new label; keep runtime/deployment uses unchanged. |
|
||||
@@ -4,6 +4,23 @@ description: "Formbricks Self-hosted version migration"
|
||||
icon: "arrow-right"
|
||||
---
|
||||
|
||||
## v5
|
||||
|
||||
Formbricks v5 changes how rate limiting is enforced:
|
||||
|
||||
- several public and API-key routes are no longer rate-limited inside the application server
|
||||
- those routes are now expected to be protected by Envoy Gateway or an equivalent edge rate limiter
|
||||
- the remaining session-based routes, server actions, and uncovered APIs still use the in-app limiter
|
||||
|
||||
<Warning>
|
||||
If you self-host Formbricks without Envoy or another equivalent edge rate limiter, upgrade planning for v5
|
||||
must include new edge protection for the covered routes. Otherwise those routes will no longer be throttled
|
||||
by the application server after the upgrade.
|
||||
</Warning>
|
||||
|
||||
See the [rate-limiting guide](/self-hosting/advanced/rate-limiting) for the exact covered route groups, thresholds,
|
||||
and the remaining app-enforced limits.
|
||||
|
||||
## v4.7
|
||||
|
||||
Formbricks v4.7 introduces **typed contact attributes** with native `number` and `date` data types. This enables comparison-based segment filters (e.g. "signup date before 2025-01-01") that were previously not possible with string-only attribute values.
|
||||
@@ -39,6 +56,7 @@ When Formbricks v4.7 starts for the first time, the data migration will:
|
||||
If you run into "**No such container**", use `docker ps` to find your container name, e.g.
|
||||
`formbricks_postgres_1`.
|
||||
</Info>
|
||||
|
||||
</Tab>
|
||||
<Tab title="Kubernetes">
|
||||
If you are using the **in-cluster PostgreSQL** deployed by the Helm chart:
|
||||
@@ -52,6 +70,7 @@ When Formbricks v4.7 starts for the first time, the data migration will:
|
||||
</Info>
|
||||
|
||||
If you are using a **managed PostgreSQL** service (e.g. AWS RDS, Cloud SQL), use your provider's backup/snapshot feature or run `pg_dump` directly against the external host.
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -69,6 +88,7 @@ When Formbricks v4.7 starts for the first time, the data migration will:
|
||||
# Start with Formbricks v4.7
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="Kubernetes">
|
||||
```bash
|
||||
@@ -81,6 +101,7 @@ When Formbricks v4.7 starts for the first time, the data migration will:
|
||||
The Helm chart includes a migration Job that automatically runs Prisma schema migrations as a
|
||||
PreSync hook before the new pods start. No manual migration step is needed.
|
||||
</Info>
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -105,6 +126,7 @@ After Formbricks starts, check the logs to see whether the value backfill was co
|
||||
```bash
|
||||
kubectl logs -n formbricks job/formbricks-migration
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -121,6 +143,7 @@ If the migration skipped the value backfill, run the standalone backfill script
|
||||
```
|
||||
|
||||
<Info>Replace `formbricks` with your actual container name if it differs. Use `docker ps` to find it.</Info>
|
||||
|
||||
</Tab>
|
||||
<Tab title="Kubernetes">
|
||||
```bash
|
||||
@@ -130,6 +153,7 @@ If the migration skipped the value backfill, run the standalone backfill script
|
||||
<Info>
|
||||
If your Formbricks deployment has a different name, run `kubectl get deploy -n formbricks` to find it.
|
||||
</Info>
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
|
||||
@@ -6,65 +6,106 @@ icon: "timer"
|
||||
|
||||
Formbricks applies request rate limits to protect against abuse and keep API usage fair.
|
||||
|
||||
Rate limits are scoped by identifier, depending on the endpoint:
|
||||
Starting with Formbricks v5, rate limiting is split across two layers:
|
||||
|
||||
- Envoy Gateway for public and API-key routes that can be enforced at ingress
|
||||
- The application server for remaining session-authenticated routes, server actions, and other flows Envoy does
|
||||
not currently cover
|
||||
|
||||
<Warning>
|
||||
Formbricks v5 removes application-level rate limiting for several routes that are now expected to be
|
||||
protected by Envoy Gateway. If you self-host Formbricks without Envoy or an equivalent edge rate limiter,
|
||||
those routes will no longer be throttled by the application server after upgrading.
|
||||
</Warning>
|
||||
|
||||
Rate limits are scoped by identifier, depending on the endpoint and enforcement layer:
|
||||
|
||||
- IP hash (for unauthenticated/client-side routes and public actions)
|
||||
- API key ID (for authenticated API calls)
|
||||
- API key ID (for Envoy-managed and app-managed authenticated API calls)
|
||||
- User ID (for authenticated session-based calls and server actions)
|
||||
- Organization ID (for follow-up email dispatch)
|
||||
|
||||
When a limit is exceeded, the API returns `429 Too Many Requests`.
|
||||
|
||||
## Management API Rate Limits
|
||||
## v5 Migration Note
|
||||
|
||||
These are the current limits for Management APIs:
|
||||
Before upgrading to Formbricks v5:
|
||||
|
||||
| **Route Group** | **Limit** | **Window** | **Identifier** |
|
||||
| --- | --- | --- | --- |
|
||||
| `/api/v1/management/*` (except `/api/v1/management/storage`), `/api/v1/webhooks/*`, `/api/v1/integrations/*`, `/api/v1/management/me` | 100 requests | 1 minute | API key ID or session user ID |
|
||||
| `/api/v2/management/*` (and other v2 authenticated routes that use `authenticatedApiClient`) | 100 requests | 1 minute | API key ID |
|
||||
| `POST /api/v1/management/storage` | 5 requests | 1 minute | API key ID or session user ID |
|
||||
- deploy Envoy Gateway or an equivalent edge rate limiter for the covered routes below
|
||||
- keep application Redis/Valkey enabled for the remaining app-enforced limits
|
||||
- expect covered routes to emit gateway `429`s instead of the legacy app JSON `429`s
|
||||
|
||||
## All Enforced Limits
|
||||
For the current source of truth on covered routes and thresholds, use this page together with your deployment
|
||||
configuration.
|
||||
|
||||
| **Config** | **Limit** | **Window** | **Identifier** | **Used For** |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `auth.login` | 10 requests | 15 minutes | IP hash | Email/password login flow (`/api/auth/callback/credentials`) |
|
||||
| `auth.signup` | 30 requests | 60 minutes | IP hash | Signup server action |
|
||||
| `auth.forgotPassword` | 5 requests | 60 minutes | IP hash | Forgot password server action |
|
||||
| `auth.verifyEmail` | 10 requests | 60 minutes | IP hash | Email verification callback + resend verification action |
|
||||
| `api.v1` | 100 requests | 1 minute | API key ID or session user ID | v1 management, webhooks, integrations, and `/api/v1/management/me` |
|
||||
| `api.v2` | 100 requests | 1 minute | API key ID | v2 authenticated API wrapper (`authenticatedApiClient`) |
|
||||
| `api.client` | 100 requests | 1 minute | IP hash | v1 client API routes (except `/api/v1/client/og` and storage upload override), plus v2 routes that re-use those v1 handlers |
|
||||
| `storage.upload` | 5 requests | 1 minute | IP hash or authenticated ID | Client storage upload and management storage upload |
|
||||
| `storage.delete` | 5 requests | 1 minute | API key ID or session user ID | `DELETE /storage/[environmentId]/[accessType]/[fileName]` |
|
||||
| `actions.emailUpdate` | 3 requests | 60 minutes | User ID | Profile email update action |
|
||||
| `actions.surveyFollowUp` | 50 requests | 60 minutes | Organization ID | Survey follow-up email processing |
|
||||
| `actions.sendLinkSurveyEmail` | 10 requests | 60 minutes | IP hash | Link survey email send action |
|
||||
| `actions.licenseRecheck` | 5 requests | 1 minute | User ID | Enterprise license recheck action |
|
||||
## Envoy-Managed Limits
|
||||
|
||||
## Current Endpoint Exceptions
|
||||
These limits are expected to be enforced at the gateway layer in Formbricks v5 and later:
|
||||
|
||||
The following routes are currently not rate-limited by the server-side limiter:
|
||||
| **Route Group** | **Limit** | **Window** | **Identifier** |
|
||||
| ------------------------------------------------------------------------ | ------------ | ---------- | -------------- |
|
||||
| `POST /api/auth/callback/credentials` | 40 requests | 1 hour | IP hash |
|
||||
| `POST /api/auth/callback/token` | 10 requests | 1 hour | IP hash |
|
||||
| `GET, POST, PUT, PATCH, DELETE /api/v1/management/*` (API key auth only) | 100 requests | 1 minute | API key ID |
|
||||
| `POST /api/v1/management/storage` (API key auth only) | 5 requests | 1 minute | API key ID |
|
||||
| `GET, POST, PUT, PATCH, DELETE /api/v1/webhooks/*` (API key auth only) | 100 requests | 1 minute | API key ID |
|
||||
| `GET /api/v1/client/[environmentId]/environment` | 100 requests | 1 minute | IP hash |
|
||||
| `POST /api/v1/client/[environmentId]/responses` | 100 requests | 1 minute | IP hash |
|
||||
| `PUT /api/v1/client/[environmentId]/responses/[responseId]` | 100 requests | 1 minute | IP hash |
|
||||
| `POST /api/v1/client/[environmentId]/displays` | 100 requests | 1 minute | IP hash |
|
||||
| `POST /api/v1/client/[environmentId]/user` | 100 requests | 1 minute | IP hash |
|
||||
| `POST /api/v1/client/[environmentId]/storage` | 5 requests | 1 minute | IP hash |
|
||||
| `POST /api/v2/client/[environmentId]/responses` | 100 requests | 1 minute | IP hash |
|
||||
| `PUT /api/v2/client/[environmentId]/responses/[responseId]` | 100 requests | 1 minute | IP hash |
|
||||
| `POST /api/v2/client/[environmentId]/displays` | 100 requests | 1 minute | IP hash |
|
||||
| `POST /api/v2/client/[environmentId]/storage` | 5 requests | 1 minute | IP hash |
|
||||
| `DELETE /storage/[environmentId]/public/[fileName]` (API key auth only) | 5 requests | 1 minute | API key ID |
|
||||
| `DELETE /storage/[environmentId]/private/[fileName]` (API key auth only) | 5 requests | 1 minute | API key ID |
|
||||
|
||||
- `GET /api/v1/client/og` (explicitly excluded)
|
||||
- `POST /api/v2/client/[environmentId]/responses`
|
||||
- `POST /api/v2/client/[environmentId]/displays`
|
||||
- `GET /api/v2/health`
|
||||
Session-authenticated `/api/v1/management/*`, `/api/v1/management/me`, `/api/v1/management/storage`, and
|
||||
`DELETE /storage/...` requests are **not** covered by the current Envoy policies and remain app-enforced.
|
||||
|
||||
## App-Enforced Limits
|
||||
|
||||
These are the limits that still run inside the Formbricks application server:
|
||||
|
||||
| **Config** | **Limit** | **Window** | **Identifier** | **Used For** |
|
||||
| ----------------------------- | ------------ | ---------- | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `auth.signup` | 30 requests | 60 minutes | IP hash | Signup server action |
|
||||
| `auth.forgotPassword` | 5 requests | 60 minutes | IP hash | Forgot password server action |
|
||||
| `auth.verifyEmail` | 10 requests | 60 minutes | IP hash | Resend verification server action |
|
||||
| `api.v1` | 100 requests | 1 minute | Session user ID | Session-authenticated v1 management routes, `/api/v1/management/me`, and `/api/v1/integrations/*` |
|
||||
| `api.v2` | 100 requests | 1 minute | API key ID | Authenticated v2 API wrapper outside the Envoy-managed `/api/v2/client/*` subset |
|
||||
| `api.v3` | 100 requests | 1 minute | API key ID or session user ID | v3 API wrapper |
|
||||
| `api.client` | 100 requests | 1 minute | IP hash | Uncovered client routes such as `GET /api/v1/client/og`, `GET /api/v2/client/[environmentId]/environment`, and `POST /api/v2/client/[environmentId]/user` |
|
||||
| `storage.upload` | 5 requests | 1 minute | Session user ID | Session-authenticated `POST /api/v1/management/storage` |
|
||||
| `storage.delete` | 5 requests | 1 minute | Session user ID | Session-authenticated `DELETE /storage/[environmentId]/[accessType]/[fileName]` |
|
||||
| `actions.emailUpdate` | 3 requests | 60 minutes | User ID | Profile email update action |
|
||||
| `actions.surveyFollowUp` | 50 requests | 60 minutes | Organization ID | Survey follow-up email processing |
|
||||
| `actions.sendLinkSurveyEmail` | 10 requests | 60 minutes | IP hash | Link survey email send action |
|
||||
| `actions.licenseRecheck` | 5 requests | 1 minute | User ID | Enterprise license recheck action |
|
||||
|
||||
## Explicit Envoy Exclusions
|
||||
|
||||
The current Envoy policy set explicitly excludes these routes:
|
||||
|
||||
- `GET /api/v1/client/og` (still covered by the app-level `api.client` limiter)
|
||||
- `GET /api/v2/health` (not rate-limited)
|
||||
- `OPTIONS` requests (not rate-limited)
|
||||
|
||||
## 429 Response Shape
|
||||
|
||||
v1-style endpoints return:
|
||||
Application-generated v1 `429`s return:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "too_many_requests",
|
||||
"message": "Maximum number of requests reached. Please try again later.",
|
||||
"details": {}
|
||||
"details": {},
|
||||
"message": "Maximum number of requests reached. Please try again later."
|
||||
}
|
||||
```
|
||||
|
||||
v2-style endpoints return:
|
||||
Application-generated v2/v3 `429`s return:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -75,6 +116,9 @@ v2-style endpoints return:
|
||||
}
|
||||
```
|
||||
|
||||
Envoy-generated `429`s are gateway responses and should include an `x-envoy-ratelimited` header. Their exact body
|
||||
shape is not the same stability contract as the in-app JSON responses above.
|
||||
|
||||
## Disabling Rate Limiting
|
||||
|
||||
For self-hosters, rate limiting can be disabled if necessary. We strongly recommend keeping it enabled in production.
|
||||
@@ -87,8 +131,11 @@ RATE_LIMITING_DISABLED=1
|
||||
|
||||
After changing this value, restart the server.
|
||||
|
||||
This setting disables only the **application-level** limiter. It does **not** disable Envoy rate-limit policies.
|
||||
|
||||
## Operational Notes
|
||||
|
||||
- Redis/Valkey is required for robust rate limiting (`REDIS_URL`).
|
||||
- If Redis is unavailable at runtime, rate-limiter checks currently fail open (requests are allowed through without enforcement).
|
||||
- Redis/Valkey is required for the application-level limiter (`REDIS_URL`).
|
||||
- If you deploy Envoy rate limiting, use a dedicated Redis/Valkey backend for Envoy instead of sharing the app cache.
|
||||
- If application Redis is unavailable at runtime, app rate-limiter checks currently fail open (requests are allowed through without enforcement).
|
||||
- Authentication failure audit logging uses a separate throttle (`shouldLogAuthFailure()`) and is intentionally **fail-closed**: when Redis is unavailable or errors occur, audit log entries are **skipped entirely** rather than written without throttle control. This prevents spam while preserving the hash-integrity chain required for compliance. In other words, if Redis is down, no authentication-failure audit logs will be recorded—requests themselves are still allowed (fail-open rate limiting above), but the audit trail for those failures will not be written.
|
||||
|
||||
Reference in New Issue
Block a user