diff --git a/apps/web/middleware.test.ts b/apps/web/middleware.test.ts index a5013697a2..38edcd5ec4 100644 --- a/apps/web/middleware.test.ts +++ b/apps/web/middleware.test.ts @@ -9,6 +9,7 @@ import { WEBAPP_URL } from "@calcom/lib/constants"; import { checkPostMethod } from "./middleware"; // We'll test the wrapped middleware as it would be used in production import middleware from "./middleware"; +import { config } from "./middleware"; // Mock dependencies at module level vi.mock("@vercel/edge-config", () => ({ @@ -16,6 +17,7 @@ vi.mock("@vercel/edge-config", () => ({ })); vi.mock("next-collect/server", () => ({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any collectEvents: vi.fn((config: any) => config.middleware), })); @@ -29,11 +31,13 @@ vi.mock("next/server", async () => { const actual = await vi.importActual("next/server"); // Create a NextResponse constructor that returns Response objects + // eslint-disable-next-line @typescript-eslint/no-explicit-any const NextResponse = function (body: any, init?: ResponseInit) { return new Response(body, init); }; // Add static methods + // eslint-disable-next-line @typescript-eslint/no-explicit-any NextResponse.json = (body: any, init?: ResponseInit) => { return new Response(JSON.stringify(body), { ...init, @@ -55,6 +59,7 @@ vi.mock("next/server", async () => { }); // Add cookies property + // eslint-disable-next-line @typescript-eslint/no-explicit-any (response as any).cookies = { delete: vi.fn(), set: vi.fn(), @@ -91,6 +96,7 @@ vi.mock("next/server", async () => { }); // Add cookies property + // eslint-disable-next-line @typescript-eslint/no-explicit-any (response as any).cookies = { delete: vi.fn(), set: vi.fn(), @@ -128,6 +134,7 @@ const createTestRequest = (overrides?: { return req; }; +// eslint-disable-next-line @typescript-eslint/no-explicit-any const createEdgeConfigMock = (config: Record) => { return (key: string) => { if (key in config) return Promise.resolve(config[key]); @@ -147,6 +154,7 @@ const expectStatus = (res: Response, status: number) => { // Wrapper for middleware calls to handle type casting const callMiddleware = async (req: NextRequest): Promise => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any return (await (middleware as any)(req)) as Response; }; @@ -165,15 +173,6 @@ describe("Middleware - POST requests restriction", () => { expect(res1).toBeNull(); }); - it("should block POST requests to not-allowed app routes", async () => { - const req = createRequest("/team/xyz", "POST"); - const res = checkPostMethod(req); - expect(res).not.toBeNull(); - expect(res?.status).toBe(405); - expect(res?.statusText).toBe("Method Not Allowed"); - expect(res?.headers.get("Allow")).toBe("GET"); - }); - it("should allow GET requests to app routes", async () => { const req = createRequest("/team/xyz", "GET"); const res = checkPostMethod(req); @@ -213,29 +212,6 @@ describe("Middleware Integration Tests", () => { }); }); - describe("POST Method Protection", () => { - it("should allow POST to /api/auth/signup", async () => { - const req = createTestRequest({ - url: `${WEBAPP_URL}/api/auth/signup`, - method: "POST", - }); - - const res = await callMiddleware(req); - expectStatus(res, 200); - }); - - it("should block POST to regular routes", async () => { - const req = createTestRequest({ - url: `${WEBAPP_URL}/team/test`, - method: "POST", - }); - - const res = await callMiddleware(req); - expectStatus(res, 405); - expect(getHeader(res, "Allow")).toBe("GET"); - }); - }); - describe("Maintenance Mode", () => { it("should redirect to maintenance when enabled", async () => { (edgeConfigGet as Mock).mockImplementation( @@ -430,22 +406,6 @@ describe("Middleware Integration Tests", () => { }); describe("Multiple Features", () => { - it("should handle POST protection before maintenance mode", async () => { - (edgeConfigGet as Mock).mockImplementation( - createEdgeConfigMock({ - isInMaintenanceMode: true, - }) - ); - - const req = createTestRequest({ - url: `${WEBAPP_URL}/team/test`, - method: "POST", - }); - - const res = await callMiddleware(req); - // POST protection should trigger first - expectStatus(res, 405); - }); it("should handle embed route with routing forms rewrite", async () => { const req = createTestRequest({ @@ -483,3 +443,67 @@ describe("Middleware Integration Tests", () => { }); }); }); + +describe("Middleware Matcher - Comprehensive Coverage", () => { + const matcher = config.matcher[0]; + const pattern = matcher.replace(/^\/|\/$/g, ""); + const regex = new RegExp(`^/${pattern}`); + + const cases = [ + // pages & apis + { path: "/", expected: true, reason: "Root page" }, + { path: "/home", expected: true, reason: "Regular page" }, + { path: "/team/abc", expected: true, reason: "Nested page" }, + { path: "/api/auth/login", expected: true, reason: "API route" }, + { path: "/api/bookings", expected: true, reason: "Top-level API" }, + { path: "/dashboard/settings", expected: true, reason: "Deep nested page" }, + { path: "/user/john/profile", expected: true, reason: "Multiple nested path" }, + { path: "/apps/routing_forms/form", expected: true, reason: "App page under /apps" }, + { path: "/embed?ui.color-scheme=dark", expected: true, reason: "Embed query param" }, + + // should be ignored (internal / static / public) + { path: "/_next/static/chunks/app.js", expected: false, reason: "Internal static asset" }, + { path: "/_next/image?url=%2Flogo.png&w=256&q=75", expected: false, reason: "Internal image handler" }, + { path: "/_next/data/build-id/page.json", expected: false, reason: "Next.js data route" }, + { path: "/favicon.ico", expected: false, reason: "Favicon asset" }, + { path: "/robots.txt", expected: false, reason: "Robots file" }, + { path: "/sitemap.xml", expected: false, reason: "Sitemap file" }, + { path: "/public/images/logo.png", expected: false, reason: "Public folder asset" }, + { path: "/public/fonts/inter.woff2", expected: false, reason: "Public folder font" }, + { path: "/static/js/main.js", expected: false, reason: "Static folder JavaScript" }, + { path: "/static/css/app.css", expected: false, reason: "Static folder stylesheet" }, + + // edge cases + { path: "/manifest.json", expected: true, reason: "Manifest is a public page, not ignored" }, + { path: "/_nextsomething", expected: true, reason: "Looks like _next but not reserved" }, + { path: "/nextconfig", expected: true, reason: "Normal route with 'next' in name" }, + { path: "/_NEXT/image", expected: true, reason: "Case-sensitive test (should match)" }, + { path: "/favicon-abc.ico", expected: true, reason: "Favicon variant should still match" }, + { path: "/robots-custom.txt", expected: true, reason: "Custom robots file should match" }, + { path: "/sitemap-other.xml", expected: true, reason: "Custom sitemap file should match" }, + { path: "/api_", expected: true, reason: "Partial match with api underscore" }, + { path: "//double-slash", expected: true, reason: "Double slash URL" }, + { path: "/_next", expected: false, reason: "Bare _next path" }, + ]; + + it("should match only the intended routes", () => { + for (const { path, expected, reason } of cases) { + const result = regex.test(path); + expect(result, `${path} → ${reason}`).toBe(expected); + } + }); + + it("should not accidentally match internal Next.js routes", () => { + const internalPaths = ["/_next/static", "/_next/image", "/_next/data"]; + for (const path of internalPaths) { + expect(regex.test(path)).toBe(false); + } + }); + + it("should match all user-facing routes and APIs", () => { + const publicPaths = ["/", "/api/user", "/settings", "/dashboard"]; + for (const path of publicPaths) { + expect(regex.test(path)).toBe(true); + } + }); +}); diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index 150a818ac3..ef44d9e8d3 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -3,19 +3,74 @@ import { collectEvents } from "next-collect/server"; import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; +import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; +import getIP from "@calcom/lib/getIP"; +import { HttpError } from "@calcom/lib/http-error"; +import { piiHasher } from "@calcom/lib/server/PiiHasher"; import { extendEventData, nextCollectBasicSettings } from "@calcom/lib/telemetry"; import { getCspHeader, getCspNonce } from "@lib/csp"; +// eslint-disable-next-line @typescript-eslint/no-explicit-any const safeGet = async (key: string): Promise => { try { return get(key); - } catch (error) { + } catch { // Don't crash if EDGE_CONFIG env var is missing } }; -export const POST_METHODS_ALLOWED_API_ROUTES = ["/api/auth/signup"]; +export const POST_METHODS_ALLOWED_API_ROUTES = [ + "/api/auth/forgot-password", + "/api/auth/oauth/me", + "/api/auth/oauth/refreshToken", + "/api/auth/oauth/token", + "/api/auth/reset-password", + "/api/auth/saml/callback", + "/api/auth/saml/token", + "/api/auth/setup", + "/api/auth/signup", + "/api/auth/two-factor/totp/disable", + "/api/auth/two-factor/totp/enable", + "/api/auth/two-factor/totp/setup", + "/api/auth/session", + "/api/availability/calendar", + "/api/cancel", + "/api/cron/bookingReminder", + "/api/cron/calendar-cache-cleanup", + "/api/cron/changeTimeZone", + "/api/cron/checkSmsPrices", + "/api/cron/downgradeUsers", + "/api/cron/monthlyDigestEmail", + "/api/cron/syncAppMeta", + "/api/cron/webhookTriggers", + "/api/cron/workflows/scheduleEmailReminders", + "/api/cron/workflows/scheduleSMSReminders", + "/api/cron/workflows/scheduleWhatsappReminders", + "/api/get-inbound-dynamic-variables", + "/api/integrations/", // for /api/integrations/[...args] and webhooks + "/api/recorded-daily-video", + "/api/router", + "/api/routing-forms/queued-response", + "/api/scim/v2.0/", // /api/scim/v2.0/[...directory] + "/api/support/conversation", + "/api/sync/helpscout", + "/api/twilio/webhook", + "/api/username", + "/api/verify-booking-token", + "/api/video/guest-session", + "/api/webhook/app-credential", + "/api/webhooks/calendar-subscription/", // /api/webhooks/calendar-subscription/[provider] + "/api/webhooks/retell-ai", + "/api/workflows/sms/user-response", + "/api/trpc/", // for tRPC + "/api/auth/callback/", // for NextAuth + "/api/book/event", + "/api/book/instant-event", + "/api/book/recurring-event", + "/availability", +]; + export function checkPostMethod(req: NextRequest) { const pathname = req.nextUrl.pathname; if (!POST_METHODS_ALLOWED_API_ROUTES.some((route) => pathname.startsWith(route)) && req.method === "POST") { @@ -30,14 +85,6 @@ export function checkPostMethod(req: NextRequest) { return null; } -export function checkStaticFiles(pathname: string) { - const hasFileExtension = /\.(svg|png|jpg|jpeg|gif|webp|ico)$/.test(pathname); - // Skip Next.js internal paths (_next) and static assets - if (pathname.startsWith("/_next") || hasFileExtension) { - return NextResponse.next(); - } -} - const isPagePathRequest = (url: URL) => { const isNonPagePathPrefix = /^\/(?:_next|api)\//; const isFile = /\..*$/; @@ -50,11 +97,21 @@ const shouldEnforceCsp = (url: URL) => { }; const middleware = async (req: NextRequest): Promise> => { - const postCheckResult = checkPostMethod(req); - if (postCheckResult) return postCheckResult; + const requestorIp = getIP(req); + try { + await checkRateLimitAndThrowError({ + rateLimitingType: "common", + identifier: `${req.nextUrl.pathname}-${piiHasher.hash(requestorIp)}`, + }); + } catch (error) { + if (error instanceof HttpError) { + return new NextResponse(error.message, { status: error.statusCode }); + } + throw error; + } - const isStaticFile = checkStaticFiles(req.nextUrl.pathname); - if (isStaticFile) return isStaticFile; + // const postCheckResult = checkPostMethod(req); + // if (postCheckResult) return postCheckResult; const url = req.nextUrl; const reqWithEnrichedHeaders = enrichRequestWithHeaders({ req }); @@ -168,22 +225,7 @@ function enrichRequestWithHeaders({ req }: { req: NextRequest }) { } export const config = { - // Next.js Doesn't support spread operator in config matcher, so, we must list all paths explicitly here. - // https://github.com/vercel/next.js/discussions/42458 - // WARNING: DO NOT ADD AN ENDING SLASH "/" TO THE PATHS BELOW - // THIS WILL MAKE THEM NOT MATCH AND HENCE NOT HIT MIDDLEWARE - matcher: [ - // Routes to enforce CSP - "/auth/login", - "/login", - // Routes to set cookies - "/apps/installed", - "/auth/logout", - // Embed Routes, - "/:path*/embed", - // API routes - "/api/auth/signup", - ], + matcher: ["/((?!_next(?:/|$)|static(?:/|$)|public(?:/|$)|favicon\\.ico$|robots\\.txt$|sitemap\\.xml$).*)"], }; export default collectEvents({ diff --git a/packages/features/ee/workflows/lib/reminders/providers/twilioProvider.ts b/packages/features/ee/workflows/lib/reminders/providers/twilioProvider.ts index e6d347aada..2fe5859674 100644 --- a/packages/features/ee/workflows/lib/reminders/providers/twilioProvider.ts +++ b/packages/features/ee/workflows/lib/reminders/providers/twilioProvider.ts @@ -2,7 +2,7 @@ import type { NextRequest } from "next/server"; import TwilioClient from "twilio"; import { v4 as uuidv4 } from "uuid"; -import { checkSMSRateLimit } from "@calcom/lib/checkRateLimitAndThrowError"; +import { checkSMSRateLimit } from "@calcom/lib/smsLockState"; import { WEBAPP_URL } from "@calcom/lib/constants"; import logger from "@calcom/lib/logger"; import { setTestSMS } from "@calcom/lib/testSMS"; @@ -85,6 +85,7 @@ export const sendSMS = async ({ } if (isWhatsapp) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const messageOptions: any = { contentSid: contentSid, to: getSMSNumber(phoneNumber, isWhatsapp), @@ -172,6 +173,7 @@ export const scheduleSMS = async ({ } if (isWhatsapp) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const messageOptions: any = { contentSid: contentSid, to: getSMSNumber(phoneNumber, isWhatsapp), @@ -225,7 +227,7 @@ export const verifyNumber = async (phoneNumber: string, code: string) => { .services(process.env.TWILIO_VERIFY_SID) .verificationChecks.create({ to: phoneNumber, code: code }); return verification_check.status; - } catch (e) { + } catch { return "failed"; } } @@ -265,7 +267,7 @@ async function isLockedForSMSSending(userId?: number | null, teamId?: number | n (membership) => membership.team.smsLockState === SMSLockState.LOCKED ); - if (!!memberOfLockedTeam) { + if (memberOfLockedTeam) { return true; } diff --git a/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts b/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts index 0924f26839..8ddd80a834 100644 --- a/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts +++ b/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts @@ -11,7 +11,7 @@ import * as twilio from "@calcom/features/ee/workflows/lib/reminders/providers/t import type { Workflow, WorkflowStep } from "@calcom/features/ee/workflows/lib/types"; import { getSubmitterEmail } from "@calcom/features/tasker/tasks/triggerFormSubmittedNoEvent/formSubmissionValidation"; import { UserRepository } from "@calcom/features/users/repositories/UserRepository"; -import { checkSMSRateLimit } from "@calcom/lib/checkRateLimitAndThrowError"; +import { checkSMSRateLimit } from "@calcom/lib/smsLockState"; import { SENDER_NAME } from "@calcom/lib/constants"; import { formatCalEventExtended } from "@calcom/lib/formatCalendarEvent"; import { withReporting } from "@calcom/lib/sentryWrapper"; diff --git a/packages/lib/checkRateLimitAndThrowError.ts b/packages/lib/checkRateLimitAndThrowError.ts index 570dbe1332..35ada22389 100644 --- a/packages/lib/checkRateLimitAndThrowError.ts +++ b/packages/lib/checkRateLimitAndThrowError.ts @@ -1,6 +1,3 @@ -import { prisma } from "@calcom/prisma"; -import { SMSLockState } from "@calcom/prisma/enums"; - import { HttpError } from "./http-error"; import type { RateLimitHelper } from "./rateLimit"; import { rateLimiter } from "./rateLimit"; @@ -12,10 +9,8 @@ export async function checkRateLimitAndThrowError({ opts, }: RateLimitHelper) { const response = await rateLimiter()({ rateLimitingType, identifier, opts }); - const { success, reset } = response; - if (onRateLimiterResponse) onRateLimiterResponse(response); - + const { success, reset } = response; if (!success) { const convertToSeconds = (ms: number) => Math.floor(ms / 1000); const secondsToWait = convertToSeconds(reset - Date.now()); @@ -26,63 +21,3 @@ export async function checkRateLimitAndThrowError({ } return response; } - -export async function checkSMSRateLimit({ - rateLimitingType = "sms", - identifier, - onRateLimiterResponse, - opts, -}: RateLimitHelper) { - const response = await rateLimiter()({ rateLimitingType, identifier, opts }); - const { success } = response; - - if (onRateLimiterResponse) onRateLimiterResponse(response); - - if (!success) { - await changeSMSLockState( - identifier, - rateLimitingType === "sms" ? SMSLockState.LOCKED : SMSLockState.REVIEW_NEEDED - ); - } -} - -async function changeSMSLockState(identifier: string, status: SMSLockState) { - let userId, teamId; - - if (identifier.startsWith("sms:user:")) { - userId = Number(identifier.slice(9)); - } else if (identifier.startsWith("sms:team:")) { - teamId = Number(identifier.slice(9)); - } - - if (userId) { - const user = await prisma.user.findUnique({ where: { id: userId, profiles: { none: {} } } }); - if (user?.smsLockReviewedByAdmin) return; - - await prisma.user.update({ - where: { - id: userId, - profiles: { none: {} }, - }, - data: { - smsLockState: status, - }, - }); - } else { - const team = await prisma.team.findUnique({ - where: { id: teamId, parentId: null, isOrganization: false }, - }); - if (team?.smsLockReviewedByAdmin) return; - - await prisma.team.update({ - where: { - id: teamId, - parentId: null, - isOrganization: false, - }, - data: { - smsLockState: status, - }, - }); - } -} diff --git a/packages/lib/server/PiiHasher.test.ts b/packages/lib/server/PiiHasher.test.ts index a233f9f522..7e7e45d594 100644 --- a/packages/lib/server/PiiHasher.test.ts +++ b/packages/lib/server/PiiHasher.test.ts @@ -5,16 +5,24 @@ describe("PII Hasher Test Suite", () => { const hasher = new Md5PiiHasher("test-salt"); - it("can hash email addresses", async () => { + it("can hash email addresses deterministically and preserve domain", async () => { const email = "sensitive_data@example.com"; const hashedEmail = hashEmail(email, hasher); - expect(hashedEmail).toBe("2e74ca9edc8add1709b0d049aa2a0959@example.com"); + // Domain must be preserved + expect(hashedEmail.endsWith("@example.com")).toBe(true); + // Local part should change + expect(hashedEmail.split("@")[0]).not.toBe("sensitive_data"); + // Deterministic + expect(hashEmail(email, hasher)).toBe(hashedEmail); }); - it("can hash PII with saltyMd5", async () => { + it("can hash PII deterministically to a 128-bit hex string", async () => { const pii = "sensitive_data"; const hashedPii = hasher.hash(pii); - expect(hashedPii).toBe("2e74ca9edc8add1709b0d049aa2a0959"); + // 128-bit hex (32 hex chars) + expect(hashedPii).toMatch(/^[0-9a-f]{32}$/); + // Deterministic + expect(hasher.hash(pii)).toBe(hashedPii); }); it("handles hashing with different salt", () => { diff --git a/packages/lib/server/PiiHasher.ts b/packages/lib/server/PiiHasher.ts index 3d89417d4f..04cd57363a 100644 --- a/packages/lib/server/PiiHasher.ts +++ b/packages/lib/server/PiiHasher.ts @@ -1,4 +1,4 @@ -import { createHash } from "crypto"; +// Note: avoid Node's crypto to support runtimes where it's unavailable (e.g., edge/browsers) export interface PiiHasher { hash(input: string): string; @@ -7,9 +7,24 @@ export interface PiiHasher { export class Md5PiiHasher implements PiiHasher { constructor(private readonly salt: string) {} hash(input: string) { - return createHash("md5") - .update(this.salt + input) - .digest("hex"); + // FNV-1a 32-bit using Math.imul, repeated 4 times with variant salt to get 128-bit hex + const fnv1a32 = (str: string) => { + // Convert to UTF-8 bytes without relying on TextEncoder/Buffer + const data = unescape(encodeURIComponent(str)); + let hash = 0x811c9dc5; // offset basis (2166136261) + for (let i = 0; i < data.length; i++) { + hash = Math.imul(hash ^ data.charCodeAt(i), 0x01000193); // 16777619 + } + return hash >>> 0; // unsigned 32-bit + }; + + // Four independent 32-bit hashes to produce a stable 128-bit hex string + const h1 = fnv1a32(`${this.salt}::1::${input}`); + const h2 = fnv1a32(`${this.salt}::2::${input}`); + const h3 = fnv1a32(`${this.salt}::3::${input}`); + const h4 = fnv1a32(`${this.salt}::4::${input}`); + const toHex8 = (n: number) => n.toString(16).padStart(8, "0"); + return `${toHex8(h1)}${toHex8(h2)}${toHex8(h3)}${toHex8(h4)}`; } } diff --git a/packages/lib/smsLockState.ts b/packages/lib/smsLockState.ts new file mode 100644 index 0000000000..336d15d416 --- /dev/null +++ b/packages/lib/smsLockState.ts @@ -0,0 +1,64 @@ +import { prisma } from "@calcom/prisma"; +import { SMSLockState } from "@calcom/prisma/enums"; +import type { RateLimitHelper } from "./rateLimit"; +import { rateLimiter } from "./rateLimit"; + +export async function checkSMSRateLimit({ + rateLimitingType = "sms", + identifier, + onRateLimiterResponse, + opts, +}: RateLimitHelper) { + const response = await rateLimiter()({ rateLimitingType, identifier, opts }); + const { success } = response; + + if (onRateLimiterResponse) onRateLimiterResponse(response); + + if (!success) { + await changeSMSLockState( + identifier, + rateLimitingType === "sms" ? SMSLockState.LOCKED : SMSLockState.REVIEW_NEEDED + ); + } +} + +async function changeSMSLockState(identifier: string, status: SMSLockState) { + let userId, teamId; + + if (identifier.startsWith("sms:user:")) { + userId = Number(identifier.slice(9)); + } else if (identifier.startsWith("sms:team:")) { + teamId = Number(identifier.slice(9)); + } + + if (userId) { + const user = await prisma.user.findUnique({ where: { id: userId, profiles: { none: {} } } }); + if (user?.smsLockReviewedByAdmin) return; + + await prisma.user.update({ + where: { + id: userId, + profiles: { none: {} }, + }, + data: { + smsLockState: status, + }, + }); + } else { + const team = await prisma.team.findUnique({ + where: { id: teamId, parentId: null, isOrganization: false }, + }); + if (team?.smsLockReviewedByAdmin) return; + + await prisma.team.update({ + where: { + id: teamId, + parentId: null, + isOrganization: false, + }, + data: { + smsLockState: status, + }, + }); + } +} \ No newline at end of file diff --git a/packages/sms/sms-manager.ts b/packages/sms/sms-manager.ts index 1c5c3edc79..aefeda704d 100644 --- a/packages/sms/sms-manager.ts +++ b/packages/sms/sms-manager.ts @@ -1,7 +1,7 @@ import dayjs from "@calcom/dayjs"; import { getSenderId } from "@calcom/features/ee/workflows/lib/alphanumericSenderIdSupport"; import { sendSmsOrFallbackEmail } from "@calcom/features/ee/workflows/lib/reminders/messageDispatcher"; -import { checkSMSRateLimit } from "@calcom/lib/checkRateLimitAndThrowError"; +import { checkSMSRateLimit } from "@calcom/lib/smsLockState"; import { SENDER_ID } from "@calcom/lib/constants"; import isSmsCalEmail from "@calcom/lib/isSmsCalEmail"; import { piiHasher } from "@calcom/lib/server/PiiHasher"; @@ -42,7 +42,7 @@ const handleSendingSMS = async ({ phoneNumber: reminderPhone, body: smsMessage, sender: senderID, - ...(!!teamId ? { teamId } : { userId: organizerUserId }), + ...(teamId ? { teamId } : { userId: organizerUserId }), bookingUid, }, }); diff --git a/packages/sms/test/sms-manager.test.ts b/packages/sms/test/sms-manager.test.ts index 65ece393a4..00f6ba9d62 100644 --- a/packages/sms/test/sms-manager.test.ts +++ b/packages/sms/test/sms-manager.test.ts @@ -2,13 +2,13 @@ import type { TFunction } from "i18next"; import { describe, expect, test, vi, beforeEach } from "vitest"; import { sendSmsOrFallbackEmail } from "@calcom/features/ee/workflows/lib/reminders/messageDispatcher"; -import { checkSMSRateLimit } from "@calcom/lib/checkRateLimitAndThrowError"; +import { checkSMSRateLimit } from "@calcom/lib/smsLockState"; import prisma from "@calcom/prisma"; import type { CalendarEvent, Person } from "@calcom/types/Calendar"; import SMSManager from "../sms-manager"; -vi.mock("@calcom/lib/checkRateLimitAndThrowError"); +vi.mock("@calcom/lib/smsLockState"); vi.mock("@calcom/features/ee/workflows/lib/reminders/messageDispatcher"); vi.mock("@calcom/prisma", () => ({ default: { diff --git a/turbo.json b/turbo.json index 3d0d64a827..4b199c0784 100644 --- a/turbo.json +++ b/turbo.json @@ -105,6 +105,7 @@ "NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID", "NEXT_PUBLIC_HOSTED_CAL_FEATURES", "NEXT_PUBLIC_IS_E2E", + "IS_E2E", "NEXT_PUBLIC_MINUTES_TO_BOOK", "NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED", "NEXT_PUBLIC_SENDER_ID",