chore: clean-up new cache package (#6532)

This commit is contained in:
Victor Hugo dos Santos
2025-09-12 08:16:13 -03:00
committed by GitHub
parent 3879d86f63
commit 935e24bd43
26 changed files with 101 additions and 230 deletions
+1
View File
@@ -61,6 +61,7 @@ RUN pnpm build --filter=@formbricks/database
# This mounts the secrets only during this build step without storing them in layers
RUN --mount=type=secret,id=database_url \
--mount=type=secret,id=encryption_key \
--mount=type=secret,id=redis_url \
--mount=type=secret,id=sentry_auth_token \
/tmp/read-secrets.sh pnpm build --filter=@formbricks/web...
-1
View File
@@ -151,7 +151,6 @@ export const DEBUG = env.DEBUG === "1";
export const ENTERPRISE_LICENSE_KEY = env.ENTERPRISE_LICENSE_KEY;
export const REDIS_URL = env.REDIS_URL;
export const REDIS_HTTP_URL = env.REDIS_HTTP_URL;
export const RATE_LIMITING_DISABLED = env.RATE_LIMITING_DISABLED === "1";
export const BREVO_API_KEY = env.BREVO_API_KEY;
+4 -3
View File
@@ -54,8 +54,10 @@ export const env = createEnv({
OIDC_ISSUER: z.string().optional(),
OIDC_SIGNING_ALGORITHM: z.string().optional(),
OPENTELEMETRY_LISTENER_URL: z.string().optional(),
REDIS_URL: z.string().optional(),
REDIS_HTTP_URL: z.string().optional(),
REDIS_URL:
process.env.NODE_ENV === "test"
? z.string().optional()
: z.string().url("REDIS_URL is required for caching, rate limiting, and audit logging"),
PASSWORD_RESET_DISABLED: z.enum(["1", "0"]).optional(),
POSTHOG_API_HOST: z.string().optional(),
POSTHOG_API_KEY: z.string().optional(),
@@ -182,7 +184,6 @@ export const env = createEnv({
OIDC_ISSUER: process.env.OIDC_ISSUER,
OIDC_SIGNING_ALGORITHM: process.env.OIDC_SIGNING_ALGORITHM,
REDIS_URL: process.env.REDIS_URL,
REDIS_HTTP_URL: process.env.REDIS_HTTP_URL,
PASSWORD_RESET_DISABLED: process.env.PASSWORD_RESET_DISABLED,
PRIVACY_URL: process.env.PRIVACY_URL,
RATE_LIMITING_DISABLED: process.env.RATE_LIMITING_DISABLED,
+15 -18
View File
@@ -1,15 +1,21 @@
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { deepDiff, redactPII, sanitizeUrlForLogging } from "./logger-helpers";
// Patch redis multi before any imports
// Patch cache before any imports
beforeEach(async () => {
const redis = (await import("@/modules/cache/redis")).default;
if ((redis?.multi as any)?.mockReturnValue) {
(redis?.multi as any).mockReturnValue({
set: vi.fn(),
exec: vi.fn().mockResolvedValue([["OK"]]),
});
}
// Mock the cache service for tests
vi.doMock("@/lib/cache", () => ({
cache: {
getRedisClient: vi.fn().mockResolvedValue({
multi: vi.fn().mockReturnValue({
set: vi.fn(),
exec: vi.fn().mockResolvedValue([["OK"]]),
}),
watch: vi.fn().mockResolvedValue("OK"),
get: vi.fn().mockResolvedValue(null),
}),
},
}));
});
vi.mock("@/modules/ee/license-check/lib/utils", () => ({
@@ -37,16 +43,7 @@ vi.mock("@/modules/ee/audit-logs/lib/service", () => ({
logAuditEvent: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("@/modules/cache/redis", () => ({
default: {
watch: vi.fn().mockResolvedValue("OK"),
multi: vi.fn().mockReturnValue({
set: vi.fn(),
exec: vi.fn().mockResolvedValue([["OK"]]),
}),
get: vi.fn().mockResolvedValue(null),
},
}));
// Cache mock is handled in beforeEach above
// Set ENCRYPTION_KEY for all tests unless explicitly testing its absence
process.env.ENCRYPTION_KEY = "testsecret";
-58
View File
@@ -1,58 +0,0 @@
import { beforeEach, describe, expect, test, vi } from "vitest";
describe("in-memory rate limiter", () => {
test("allows requests within limit and throws after limit", async () => {
const { rateLimit } = await import("./rate-limit");
const limiterFn = rateLimit({ interval: 1, allowedPerInterval: 2 });
await expect(limiterFn("a")).resolves.toBeUndefined();
await expect(limiterFn("a")).resolves.toBeUndefined();
await expect(limiterFn("a")).rejects.toThrow("Rate limit exceeded");
});
test("separate tokens have separate counts", async () => {
const { rateLimit } = await import("./rate-limit");
const limiterFn = rateLimit({ interval: 1, allowedPerInterval: 2 });
await expect(limiterFn("x")).resolves.toBeUndefined();
await expect(limiterFn("y")).resolves.toBeUndefined();
await expect(limiterFn("x")).resolves.toBeUndefined();
await expect(limiterFn("y")).resolves.toBeUndefined();
});
});
describe("redis rate limiter", () => {
beforeEach(async () => {
vi.resetModules();
const constants = await vi.importActual("@/lib/constants");
vi.doMock("@/lib/constants", () => ({
...constants,
REDIS_HTTP_URL: "http://redis",
}));
});
test("sets expire on first use and does not throw", async () => {
global.fetch = vi
.fn()
.mockResolvedValueOnce({ ok: true, json: async () => ({ INCR: 1 }) })
.mockResolvedValueOnce({ ok: true });
const { rateLimit } = await import("./rate-limit");
const limiter = rateLimit({ interval: 10, allowedPerInterval: 2 });
await expect(limiter("t")).resolves.toBeUndefined();
expect(fetch).toHaveBeenCalledTimes(2);
expect(fetch).toHaveBeenCalledWith("http://redis/INCR/t");
expect(fetch).toHaveBeenCalledWith("http://redis/EXPIRE/t/10");
});
test("does not throw when redis INCR response not ok", async () => {
global.fetch = vi.fn().mockResolvedValueOnce({ ok: false });
const { rateLimit } = await import("./rate-limit");
const limiter = rateLimit({ interval: 10, allowedPerInterval: 2 });
await expect(limiter("t")).resolves.toBeUndefined();
});
test("throws when INCR exceeds limit", async () => {
global.fetch = vi.fn().mockResolvedValueOnce({ ok: true, json: async () => ({ INCR: 3 }) });
const { rateLimit } = await import("./rate-limit");
const limiter = rateLimit({ interval: 10, allowedPerInterval: 2 });
await expect(limiter("t")).rejects.toThrow("Rate limit exceeded for IP: t");
});
});
-54
View File
@@ -1,54 +0,0 @@
import { REDIS_HTTP_URL } from "@/lib/constants";
import { LRUCache } from "lru-cache";
import { logger } from "@formbricks/logger";
interface Options {
interval: number;
allowedPerInterval: number;
}
const inMemoryRateLimiter = (options: Options) => {
const tokenCache = new LRUCache<string, number>({
max: 1000,
ttl: options.interval * 1000, // converts to expected input of milliseconds
});
return async (token: string) => {
const currentUsage = tokenCache.get(token) ?? 0;
if (currentUsage >= options.allowedPerInterval) {
throw new Error("Rate limit exceeded");
}
tokenCache.set(token, currentUsage + 1);
};
};
const redisRateLimiter = (options: Options) => async (token: string) => {
try {
if (!REDIS_HTTP_URL) {
throw new Error("Redis HTTP URL is not set");
}
const tokenCountResponse = await fetch(`${REDIS_HTTP_URL}/INCR/${token}`);
if (!tokenCountResponse.ok) {
logger.error({ tokenCountResponse }, "Failed to increment token count in Redis");
return;
}
const { INCR } = await tokenCountResponse.json();
if (INCR === 1) {
await fetch(`${REDIS_HTTP_URL}/EXPIRE/${token}/${options.interval.toString()}`);
} else if (INCR > options.allowedPerInterval) {
throw new Error();
}
} catch (e) {
logger.error({ error: e }, "Rate limit exceeded");
throw new Error("Rate limit exceeded for IP: " + token);
}
};
export const rateLimit = (options: Options) => {
if (REDIS_HTTP_URL) {
return redisRateLimiter(options);
} else {
return inMemoryRateLimiter(options);
}
};
-1
View File
@@ -748,7 +748,6 @@
"api_key_label": "API-Schlüssel Label",
"api_key_security_warning": "Aus Sicherheitsgründen wird der API-Schlüssel nur einmal nach der Erstellung angezeigt. Bitte kopiere ihn sofort an einen sicheren Ort.",
"api_key_updated": "API-Schlüssel aktualisiert",
"delete_permission": "Berechtigung löschen",
"duplicate_access": "Doppelter Projektzugriff nicht erlaubt",
"no_api_keys_yet": "Du hast noch keine API-Schlüssel",
"no_env_permissions_found": "Keine Umgebungsberechtigungen gefunden",
-1
View File
@@ -748,7 +748,6 @@
"api_key_label": "API Key Label",
"api_key_security_warning": "For security reasons, the API key will only be shown once after creation. Please copy it to your destination right away.",
"api_key_updated": "API Key updated",
"delete_permission": "Delete permission",
"duplicate_access": "Duplicate project access not allowed",
"no_api_keys_yet": "You don't have any API keys yet",
"no_env_permissions_found": "No environment permissions found",
-1
View File
@@ -748,7 +748,6 @@
"api_key_label": "Étiquette de clé API",
"api_key_security_warning": "Pour des raisons de sécurité, la clé API ne sera affichée qu'une seule fois après sa création. Veuillez la copier immédiatement à votre destination.",
"api_key_updated": "Clé API mise à jour",
"delete_permission": "Supprimer une permission",
"duplicate_access": "L'accès en double au projet n'est pas autorisé",
"no_api_keys_yet": "Vous n'avez pas encore de clés API.",
"no_env_permissions_found": "Aucune autorisation d'environnement trouvée",
-1
View File
@@ -748,7 +748,6 @@
"api_key_label": "APIキーのラベル",
"api_key_security_warning": "セキュリティ上の理由から、APIキーは作成時に一度だけ表示されます。すぐにコピーしてください。",
"api_key_updated": "APIキーを更新しました",
"delete_permission": "削除権限",
"duplicate_access": "重複したプロジェクトアクセスは許可されません",
"no_api_keys_yet": "まだAPIキーがありません",
"no_env_permissions_found": "環境権限が見つかりません",
-1
View File
@@ -748,7 +748,6 @@
"api_key_label": "Rótulo da Chave API",
"api_key_security_warning": "Por motivos de segurança, a chave da API será mostrada apenas uma vez após a criação. Por favor, copie-a para o seu destino imediatamente.",
"api_key_updated": "Chave de API atualizada",
"delete_permission": "Remover permissão",
"duplicate_access": "Acesso duplicado ao projeto não permitido",
"no_api_keys_yet": "Você ainda não tem nenhuma chave de API",
"no_env_permissions_found": "Nenhuma permissão de ambiente encontrada",
-1
View File
@@ -748,7 +748,6 @@
"api_key_label": "Etiqueta da Chave API",
"api_key_security_warning": "Por razões de segurança, a chave API será mostrada apenas uma vez após a criação. Por favor, copie-a para o seu destino imediatamente.",
"api_key_updated": "Chave API atualizada",
"delete_permission": "Eliminar permissão",
"duplicate_access": "Acesso duplicado ao projeto não permitido",
"no_api_keys_yet": "Ainda não tem nenhuma chave API",
"no_env_permissions_found": "Nenhuma permissão de ambiente encontrada",
-1
View File
@@ -748,7 +748,6 @@
"api_key_label": "Etichetă Cheie API",
"api_key_security_warning": "Din motive de securitate, cheia API va fi afișată o singură dată după creare. Vă rugăm să o copiați imediat la destinație.",
"api_key_updated": "Cheie API actualizată",
"delete_permission": "Șterge permisiunea",
"duplicate_access": "Accesul dublu la proiect nu este permis",
"no_api_keys_yet": "Nu aveți încă chei API",
"no_env_permissions_found": "Nu s-au găsit permisiuni pentru mediu",
-1
View File
@@ -748,7 +748,6 @@
"api_key_label": "API Key Label",
"api_key_security_warning": "出于安全原因,API 密钥在创建后只会显示一次。 请立即将其复制到您的目标位置。",
"api_key_updated": "API Key 已更新",
"delete_permission": "删除 权限",
"duplicate_access": "不允许 复制 项目 访问",
"no_api_keys_yet": "您 还 没有 任 何 API key",
"no_env_permissions_found": "未找到环境权限",
-1
View File
@@ -748,7 +748,6 @@
"api_key_label": "API 金鑰標籤",
"api_key_security_warning": "為安全起見,API 金鑰僅在建立後顯示一次。請立即將其複製到您的目的地。",
"api_key_updated": "API 金鑰已更新",
"delete_permission": "刪除 權限",
"duplicate_access": "不允許重複的 project 存取",
"no_api_keys_yet": "您還沒有任何 API 金鑰",
"no_env_permissions_found": "找不到環境權限",
-3
View File
@@ -38,7 +38,6 @@
"@hookform/resolvers": "5.0.1",
"@intercom/messenger-js-sdk": "0.0.14",
"@json2csv/node": "7.0.6",
"@keyv/redis": "4.4.0",
"@lexical/code": "0.31.0",
"@lexical/link": "0.31.0",
"@lexical/list": "0.31.0",
@@ -85,7 +84,6 @@
"@vercel/og": "0.8.5",
"bcryptjs": "3.0.2",
"boring-avatars": "2.0.1",
"cache-manager": "6.4.3",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"cmdk": "1.1.1",
@@ -98,7 +96,6 @@
"https-proxy-agent": "7.0.6",
"jiti": "2.4.2",
"jsonwebtoken": "9.0.2",
"keyv": "5.3.3",
"lexical": "0.31.0",
"lodash": "4.17.21",
"lru-cache": "11.1.0",
+11 -2
View File
@@ -6,7 +6,7 @@ if [ -f "/run/secrets/database_url" ]; then
DATABASE_URL=${DATABASE_URL%$'\n'}
export DATABASE_URL
else
echo "DATABASE_URL secret not found. Build may fail if this is required."
echo "DATABASE_URL secret not found. Build will fail because it is required by the application."
fi
if [ -f "/run/secrets/encryption_key" ]; then
@@ -14,7 +14,15 @@ if [ -f "/run/secrets/encryption_key" ]; then
ENCRYPTION_KEY=${ENCRYPTION_KEY%$'\n'}
export ENCRYPTION_KEY
else
echo "ENCRYPTION_KEY secret not found. Build may fail if this is required."
echo "ENCRYPTION_KEY secret not found. Build will fail because it is required by the application."
fi
if [ -f "/run/secrets/redis_url" ]; then
IFS= read -r REDIS_URL < /run/secrets/redis_url || true
REDIS_URL=${REDIS_URL%$'\n'}
export REDIS_URL
else
echo "REDIS_URL secret not found. Build will fail because it is required by the application."
fi
if [ -f "/run/secrets/sentry_auth_token" ]; then
@@ -39,6 +47,7 @@ fi
# Verify environment variables are set before starting build
echo " DATABASE_URL: $([ -n "${DATABASE_URL:-}" ] && printf '[SET]' || printf '[NOT SET]')"
echo " ENCRYPTION_KEY: $([ -n "${ENCRYPTION_KEY:-}" ] && printf '[SET]' || printf '[NOT SET]')"
echo " REDIS_URL: $([ -n "${REDIS_URL:-}" ] && printf '[SET]' || printf '[NOT SET]')"
echo " SENTRY_AUTH_TOKEN: $([ -n "${SENTRY_AUTH_TOKEN:-}" ] && printf '[SET]' || printf '[NOT SET]')"
echo " TARGETARCH: $([ -n "${TARGETARCH:-}" ] && printf '%s' "${TARGETARCH}" || printf '[NOT SET]')"