refactor: apply biome formatting to packages/sms, prisma, emails, lib (#27880)

* refactor: apply biome formatting to small packages + packages/lib

Format packages/sms, packages/prisma, packages/platform/libraries,
packages/platform/examples, packages/platform/types, packages/emails,
and packages/lib.

Excludes packages/platform/examples/base/src/pages/[bookingUid].tsx
due to pre-existing lint errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* revert: remove packages/platform formatting changes

Revert biome formatting for packages/platform as requested.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eunjae LeeandClaude Opus 4.6 authored and GitHub committed 2026-02-13 07:39:01 -03:00
1 parent 7aefefc31f
commit e2119879ae
53 files changed
+301 -385

No files matched your search

+6 -21
View File
@@ -25,21 +25,13 @@ const eventTypeDisableAttendeeEmail = (metadata?: EventTypeMetadata) => {
return !!metadata?.disableStandardEmails?.all?.attendee;
};
export const sendOrganizerPaymentRefundFailedEmail = async (
calEvent: CalendarEvent
) => {
export const sendOrganizerPaymentRefundFailedEmail = async (calEvent: CalendarEvent) => {
const emailsToSend: Promise<unknown>[] = [];
emailsToSend.push(
sendEmail(() => new OrganizerPaymentRefundFailedEmail({ calEvent }))
);
emailsToSend.push(sendEmail(() => new OrganizerPaymentRefundFailedEmail({ calEvent })));
if (calEvent.team?.members) {
for (const teamMember of calEvent.team.members) {
emailsToSend.push(
sendEmail(
() => new OrganizerPaymentRefundFailedEmail({ calEvent, teamMember })
)
);
emailsToSend.push(sendEmail(() => new OrganizerPaymentRefundFailedEmail({ calEvent, teamMember })));
}
}
@@ -99,9 +91,7 @@ export const sendCreditBalanceLowWarningEmails = async (input: {
}
if (user) {
await sendEmail(
() => new CreditBalanceLowWarningEmail({ user, balance, creditFor })
);
await sendEmail(() => new CreditBalanceLowWarningEmail({ user, balance, creditFor }));
}
};
@@ -135,19 +125,14 @@ export const sendCreditBalanceLimitReachedEmails = async ({
for (const admin of team.adminAndOwners) {
emailsToSend.push(
sendEmail(
() =>
new CreditBalanceLimitReachedEmail({ user: admin, team, creditFor })
)
sendEmail(() => new CreditBalanceLimitReachedEmail({ user: admin, team, creditFor }))
);
}
await Promise.all(emailsToSend);
}
if (user) {
await sendEmail(
() => new CreditBalanceLimitReachedEmail({ user, creditFor })
);
await sendEmail(() => new CreditBalanceLimitReachedEmail({ user, creditFor }));
}
};
+5 -4
View File
@@ -9,9 +9,11 @@ import AttendeeScheduledEmail from "./templates/attendee-scheduled-email";
const mockGetEmailSettings = vi.fn();
vi.mock("@calcom/features/organizations/repositories/OrganizationSettingsRepository", () => ({
OrganizationSettingsRepository: vi.fn().mockImplementation(function() { return {
getEmailSettings: mockGetEmailSettings,
}; }),
OrganizationSettingsRepository: vi.fn().mockImplementation(function () {
return {
getEmailSettings: mockGetEmailSettings,
};
}),
}));
vi.mock("@calcom/prisma", () => ({
@@ -184,7 +186,6 @@ describe("shouldSkipAttendeeEmailWithSettings", () => {
});
describe("AttendeeScheduledEmail - Privacy fix for seated events", () => {
const createMockPerson = (name: string, email: string): Person => ({
name,
email,
+4 -1
View File
@@ -15,7 +15,10 @@ const sendEmail = async (prepare: () => BaseEmail) => {
return await email.sendEmail();
} catch (e) {
const errorName = e instanceof Error ? e.name : "UnknownError";
console.error(`${email?.constructor?.name ?? "Email"}.sendEmail oauth-email-service failed (${errorName})`, e);
console.error(
`${email?.constructor?.name ?? "Email"}.sendEmail oauth-email-service failed (${errorName})`,
e
);
throw e;
}
};
@@ -72,8 +72,8 @@ export const BaseScheduledEmail = (
props.title
? props.title
: props.calEvent.recurringEvent?.count
? "your_event_has_been_scheduled_recurring"
: "your_event_has_been_scheduled"
? "your_event_has_been_scheduled_recurring"
: "your_event_has_been_scheduled"
)}
callToAction={
props.callToAction === null
@@ -26,15 +26,25 @@ export const CreditBalanceLimitReachedEmail = (
if (team) {
return (
<V2BaseEmailHtml subject={user.t("action_required_out_of_credits", { teamName: team.name, interpolation: { escapeValue: false } })}>
<V2BaseEmailHtml
subject={user.t("action_required_out_of_credits", {
teamName: team.name,
interpolation: { escapeValue: false },
})}>
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
<> {user.t("hi_user_name", { name: user.name, interpolation: { escapeValue: false } })},</>
</p>
<p style={{ fontWeight: 400, lineHeight: "24px", marginBottom: "20px" }}>
<>
{isCalAi
? user.t("cal_ai_credit_limit_reached_message", { teamName: team.name, interpolation: { escapeValue: false } })
: user.t("credit_limit_reached_message", { teamName: team.name, interpolation: { escapeValue: false } })}
? user.t("cal_ai_credit_limit_reached_message", {
teamName: team.name,
interpolation: { escapeValue: false },
})
: user.t("credit_limit_reached_message", {
teamName: team.name,
interpolation: { escapeValue: false },
})}
</>
</p>
<div style={{ textAlign: "center", marginTop: "24px" }}>
@@ -27,15 +27,25 @@ export const CreditBalanceLowWarningEmail = (
if (team) {
return (
<V2BaseEmailHtml subject={user.t("team_credits_low_warning", { teamName: team.name, interpolation: { escapeValue: false } })}>
<V2BaseEmailHtml
subject={user.t("team_credits_low_warning", {
teamName: team.name,
interpolation: { escapeValue: false },
})}>
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
<> {user.t("hi_user_name", { name: user.name, interpolation: { escapeValue: false } })},</>
</p>
<p style={{ fontWeight: 400, lineHeight: "24px", marginBottom: "20px" }}>
<>
{isCalAi
? user.t("cal_ai_low_credits_warning_message", { teamName: team.name, interpolation: { escapeValue: false } })
: user.t("low_credits_warning_message", { teamName: team.name, interpolation: { escapeValue: false } })}
? user.t("cal_ai_low_credits_warning_message", {
teamName: team.name,
interpolation: { escapeValue: false },
})
: user.t("low_credits_warning_message", {
teamName: team.name,
interpolation: { escapeValue: false },
})}
</>
</p>
<p
@@ -35,9 +35,7 @@ export const OAuthClientApprovedNotificationEmail = ({
}}>
{language("oauth_client_approved_email_title", { clientName })}
</p>
<p style={{ fontWeight: 400 }}>
{language("hi_user", { name: userName || language("there") })}!
</p>
<p style={{ fontWeight: 400 }}>{language("hi_user", { name: userName || language("there") })}!</p>
<p style={{ fontWeight: 400, lineHeight: "24px" }}>{language("oauth_client_approved_email_body")}</p>
<table
role="presentation"
@@ -7,8 +7,10 @@ export const OrganizerCancelledEmail = (props: React.ComponentProps<typeof Organ
const title = "event_request_cancelled";
const subject = "event_cancelled_subject";
const isRoundRobin = props.calEvent.schedulingType === SchedulingType.ROUND_ROBIN;
const subtitle = props.reassigned
? (isRoundRobin ? t("event_reassigned_subtitle") : t("event_reassigned_subtitle_generic"))
const subtitle = props.reassigned
? isRoundRobin
? t("event_reassigned_subtitle")
: t("event_reassigned_subtitle_generic")
: "";
return (
<OrganizerScheduledEmail
@@ -55,7 +55,10 @@ export const OrganizerScheduledEmail = (
) : (
<>
{props.attendeeCancelled
? t("attendee_no_longer_attending_subtitle", { name: props.attendee.name, interpolation: { escapeValue: false } })
? t("attendee_no_longer_attending_subtitle", {
name: props.attendee.name,
interpolation: { escapeValue: false },
})
: ""}
</>
)
@@ -136,7 +136,7 @@ export const TeamInviteEmail = (
parentTeamName: props.parentTeamName,
prevLink: props.prevLink,
newLink: props.newLink,
orgName: props.parentTeamName ?? props.isOrg ? props.teamName : "",
orgName: (props.parentTeamName ?? props.isOrg) ? props.teamName : "",
prevLinkWithoutProtocol: props.prevLink?.replace(/https?:\/\//, ""),
newLinkWithoutProtocol: props.newLink?.replace(/https?:\/\//, ""),
};
@@ -45,10 +45,10 @@ export default class AttendeeWasRequestedToRescheduleEmail extends OrganizerSche
<p style="color: #494949;">${this.t("when")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;text-decoration: line-through;">
${this.t(this.getOrganizerStart("dddd").toLowerCase())}, ${this.t(
this.getOrganizerStart("MMMM").toLowerCase()
)} ${this.getOrganizerStart("D")}, ${this.getOrganizerStart("YYYY")} | ${this.getOrganizerStart(
"h:mma"
)} - ${this.getOrganizerEnd("h:mma")} <span style="color: #888888">(${this.getTimezone()})</span>
this.getOrganizerStart("MMMM").toLowerCase()
)} ${this.getOrganizerStart("D")}, ${this.getOrganizerStart("YYYY")} | ${this.getOrganizerStart(
"h:mma"
)} - ${this.getOrganizerEnd("h:mma")} <span style="color: #888888">(${this.getTimezone()})</span>
</p>
</div>`;
}
@@ -53,7 +53,7 @@ export default class DisabledAppEmail extends BaseEmail {
return this.appType.some((type) => type === "payment")
? this.t("disable_payment_app", { appName: this.appName, title: this.title })
: this.appType.some((type) => type === "video")
? this.t("app_disabled_video", { appName: this.appName })
: this.t("app_disabled", { appName: this.appName });
? this.t("app_disabled_video", { appName: this.appName })
: this.t("app_disabled", { appName: this.appName });
}
}
@@ -94,8 +94,8 @@ ${this.t(
title
? title
: this.calEvent.recurringEvent?.count
? "new_event_scheduled_recurring"
: "new_event_scheduled"
? "new_event_scheduled_recurring"
: "new_event_scheduled"
)}
${this.t(subtitle)}
${extraInfo}
+2 -2
View File
@@ -467,7 +467,7 @@ type RichDescriptionCalEvent = {
additionalInformation?: AdditionalInformation;
location?: string | null;
uid?: string | null;
}
};
export const getRichDescriptionHTML = (
calEvent: RichDescriptionCalEvent,
@@ -649,5 +649,5 @@ export const getVideoCallUrlFromCalEvent = (calEvent: {
};
export const getVideoCallPassword = (videoCallData?: VideoCallData): string => {
return isDailyVideoCall(videoCallData) ? "" : videoCallData?.password ?? "";
return isDailyVideoCall(videoCallData) ? "" : (videoCallData?.password ?? "");
};
+2 -2
View File
@@ -243,8 +243,8 @@ export default abstract class BaseCalendarService implements Calendar {
throw new Error(`Error creating iCalString:=> ${error?.message} : ${error?.name} `);
const mainHostDestinationCalendar = event.destinationCalendar
? event.destinationCalendar.find((cal) => cal.credentialId === credentialId) ??
event.destinationCalendar[0]
? (event.destinationCalendar.find((cal) => cal.credentialId === credentialId) ??
event.destinationCalendar[0])
: undefined;
// We create the event directly on iCal
+6 -1
View File
@@ -1,6 +1,11 @@
import { describe, expect, it } from "vitest";
import { getOGImageVersion, constructMeetingImage, constructAppImage, constructGenericImage } from "./OgImages";
import {
getOGImageVersion,
constructMeetingImage,
constructAppImage,
constructGenericImage,
} from "./OgImages";
describe("OgImages", () => {
describe("getOGImageVersion", () => {
+21 -72
View File
@@ -47,8 +47,7 @@ const joinMultipleNames = (names: string[] = []) => {
return `${names.length > 0 ? `${names.join(", ")} & ${lastName}` : lastName}`;
};
const makeAbsoluteUrl = (url: string) =>
/^https?:\/\//.test(url) ? url : `${CAL_URL}${url}`;
const makeAbsoluteUrl = (url: string) => (/^https?:\/\//.test(url) ? url : `${CAL_URL}${url}`);
const OG_ASSETS = {
meeting: {
@@ -82,20 +81,12 @@ export const getOGImageVersion = async (
...(additionalInputs ?? {}),
};
const content = JSON.stringify(
versionInputs,
Object.keys(versionInputs).sort()
);
const content = JSON.stringify(versionInputs, Object.keys(versionInputs).sort());
// Use Web Crypto API instead of Node.js crypto for Edge Runtime compatibility (`/api/social/og/image` is an Edge Runtime route)
const hashBuffer = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(content)
);
const hashBuffer = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(content));
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
return hashHex.substring(0, 8);
};
@@ -156,10 +147,7 @@ export const constructAppImage = async ({
return encodeURIComponent(`/api/social/og/image?${params.toString()}`);
};
export const constructGenericImage = async ({
title,
description,
}: GenericImageProps): Promise<string> => {
export const constructGenericImage = async ({ title, description }: GenericImageProps): Promise<string> => {
const params = new URLSearchParams({
type: "generic",
title,
@@ -171,11 +159,7 @@ export const constructGenericImage = async ({
return encodeURIComponent(`/api/social/og/image?${params.toString()}`);
};
const Wrapper = ({
children,
variant = "light",
rotateBackground,
}: WrapperProps) => (
const Wrapper = ({ children, variant = "light", rotateBackground }: WrapperProps) => (
<div tw="flex w-full h-full">
<img
tw="flex absolute left-0 top-0 w-full h-[110%]"
@@ -185,9 +169,7 @@ const Wrapper = ({
width="1200"
height="600"
/>
<div tw="flex flex-col w-full h-full px-[80px] py-[70px] items-start justify-center">
{children}
</div>
<div tw="flex flex-col w-full h-full px-[80px] py-[70px] items-start justify-center">{children}</div>
</div>
);
@@ -204,8 +186,7 @@ export const Meeting = ({ title, users = [], profile }: MeetingImageProps) => {
// any non existing images for dynamic collectives, while at the same time removing them from
// the names list, because the profile name of that event is a concatenation of all names.
const attendees = (profile.image ? [profile, ...users] : users).filter(
(value, index, self) =>
self.findIndex((v) => v.name === value.name) === index
(value, index, self) => self.findIndex((v) => v.name === value.name) === index
);
// Construct list of avatar urls, removes duplicates and empty profile images
@@ -218,26 +199,15 @@ export const Meeting = ({ title, users = [], profile }: MeetingImageProps) => {
// In case there is NO other attendee than the single meeting profile without an image, we add
// that name back in here, since the event probably is a round robin event.
const names =
attendees.length > 0 ? attendees.map((user) => user.name) : [profile.name];
const names = attendees.length > 0 ? attendees.map((user) => user.name) : [profile.name];
return (
<Wrapper variant={config.variant}>
<div tw="h-full flex flex-col justify-start">
<div
tw="flex items-center justify-center"
style={{ fontFamily: "cal", fontWeight: 300 }}
>
<img
src={`${WEBAPP_URL}/${config.logo}`}
width={config.logoWidth}
alt="Logo"
/>
<div tw="flex items-center justify-center" style={{ fontFamily: "cal", fontWeight: 300 }}>
<img src={`${WEBAPP_URL}/${config.logo}`} width={config.logoWidth} alt="Logo" />
{avatars.length > 0 && (
<div
style={{ color: "#111827" }}
tw="font-bold text-[92px] mx-8 bottom-2"
>
<div style={{ color: "#111827" }} tw="font-bold text-[92px] mx-8 bottom-2">
/
</div>
)}
@@ -254,27 +224,20 @@ export const Meeting = ({ title, users = [], profile }: MeetingImageProps) => {
))}
{avatars.length > 3 && (
<div
tw={`flex items-center justify-center w-[${config.avatarSize}px] h-[${config.avatarSize}px] rounded-full bg-black text-inverted text-[54px] font-bold`}
>
<span tw="flex top-[-5px] left-[-5px]">
+{avatars.length - 3}
</span>
tw={`flex items-center justify-center w-[${config.avatarSize}px] h-[${config.avatarSize}px] rounded-full bg-black text-inverted text-[54px] font-bold`}>
<span tw="flex top-[-5px] left-[-5px]">+{avatars.length - 3}</span>
</div>
)}
</div>
</div>
<div
style={{ color: "#111827" }}
tw="relative flex text-[54px] w-full flex-col mt-auto"
>
<div style={{ color: "#111827" }} tw="relative flex text-[54px] w-full flex-col mt-auto">
<div
tw="flex w-[1040px] overflow-hidden"
style={{
whiteSpace: "nowrap",
fontFamily: "cal",
textOverflow: "ellipsis",
}}
>
}}>
Meet {joinMultipleNames(names)}
</div>
<div
@@ -283,8 +246,7 @@ export const Meeting = ({ title, users = [], profile }: MeetingImageProps) => {
whiteSpace: "nowrap",
fontFamily: "inter",
textOverflow: "ellipsis",
}}
>
}}>
{title}
</div>
</div>
@@ -355,10 +317,7 @@ export const App = ({ name, description, logoUrl }: AppImageProps) => {
</div>
</div>
<div style={{ color: "#111827" }} tw="flex mt-auto w-full flex-col">
<div
tw="flex text-[64px] mb-7"
style={{ fontFamily: "cal", fontWeight: 600 }}
>
<div tw="flex text-[64px] mb-7" style={{ fontFamily: "cal", fontWeight: 600 }}>
{name}
</div>
<div tw="flex text-[36px]" style={{ fontFamily: "inter" }}>
@@ -380,21 +339,11 @@ export const Generic = ({ title, description }: GenericImageProps) => {
return (
<Wrapper variant={config.variant}>
<div tw="h-full flex flex-col justify-start">
<div
tw="flex items-center justify-center"
style={{ fontFamily: "cal", fontWeight: 300 }}
>
<img
src={`${WEBAPP_URL}/${config.logo}`}
width={config.logoWidth}
alt="Logo"
/>
<div tw="flex items-center justify-center" style={{ fontFamily: "cal", fontWeight: 300 }}>
<img src={`${WEBAPP_URL}/${config.logo}`} width={config.logoWidth} alt="Logo" />
</div>
<div
style={{ color: "#111827" }}
tw="relative flex text-[54px] w-full flex-col mt-auto"
>
<div style={{ color: "#111827" }} tw="relative flex text-[54px] w-full flex-col mt-auto">
<div tw="flex w-[1040px]" style={{ fontFamily: "cal" }}>
{title}
</div>
+1 -1
View File
@@ -1,4 +1,4 @@
import type { AppOnboardingSteps } from "@calcom/lib/apps/appOnboardingSteps"
import type { AppOnboardingSteps } from "@calcom/lib/apps/appOnboardingSteps";
// biome-ignore lint/style/useNodejsImportProtocol: Vite env
import { stringify } from "querystring";
+7 -4
View File
@@ -19,10 +19,13 @@ const computeInstallCountsFromDB = async (): Promise<Record<string, number>> =>
"installCount" DESC
`
);
return mostPopularApps.reduce((acc, { appId, installCount }) => {
acc[appId] = installCount;
return acc;
}, {} as Record<string, number>);
return mostPopularApps.reduce(
(acc, { appId, installCount }) => {
acc[appId] = installCount;
return acc;
},
{} as Record<string, number>
);
};
const getInstallCountPerApp = async (): Promise<Record<string, number>> => {
+1 -1
View File
@@ -28,7 +28,7 @@ export function getAvailabilityFromSchedule(schedule: Schedule): Availability[]
days: [day],
startTime: time.start,
endTime: time.end,
} as Availability);
}) as Availability;
const filteredTimes = times.filter((time) => {
let idx: number;
+2 -2
View File
@@ -109,8 +109,8 @@ export const buildCalEventFromBooking = async ({
destinationCalendar: booking.destinationCalendar
? [booking.destinationCalendar]
: booking.user?.destinationCalendar
? [booking.user?.destinationCalendar]
: [],
? [booking.user?.destinationCalendar]
: [],
seatsPerTimeSlot: booking.eventType?.seatsPerTimeSlot,
seatsShowAttendees: booking.eventType?.seatsShowAttendees,
customReplyToEmail: booking.eventType?.customReplyToEmail,
+43 -108
View File
@@ -9,23 +9,14 @@ function ensureProtocol(url: string | undefined): string {
return `https://${url}`;
}
const VERCEL_URL = process.env.NEXT_PUBLIC_VERCEL_URL
? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`
: "";
const RAILWAY_STATIC_URL = process.env.RAILWAY_STATIC_URL
? `https://${process.env.RAILWAY_STATIC_URL}`
: "";
const HEROKU_URL = process.env.HEROKU_APP_NAME
? `https://${process.env.HEROKU_APP_NAME}.herokuapp.com`
: "";
const RENDER_URL = process.env.RENDER_EXTERNAL_URL
? `https://${process.env.RENDER_EXTERNAL_URL}`
: "";
const VERCEL_URL = process.env.NEXT_PUBLIC_VERCEL_URL ? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}` : "";
const RAILWAY_STATIC_URL = process.env.RAILWAY_STATIC_URL ? `https://${process.env.RAILWAY_STATIC_URL}` : "";
const HEROKU_URL = process.env.HEROKU_APP_NAME ? `https://${process.env.HEROKU_APP_NAME}.herokuapp.com` : "";
const RENDER_URL = process.env.RENDER_EXTERNAL_URL ? `https://${process.env.RENDER_EXTERNAL_URL}` : "";
export const CALCOM_ENV = process.env.CALCOM_ENV || process.env.NODE_ENV;
export const IS_PRODUCTION = CALCOM_ENV === "production";
export const IS_PRODUCTION_BUILD = process.env.NODE_ENV === "production";
export const ORGANIZER_EMAIL_EXEMPT_DOMAINS =
process.env.ORGANIZER_EMAIL_EXEMPT_DOMAINS || "";
export const ORGANIZER_EMAIL_EXEMPT_DOMAINS = process.env.ORGANIZER_EMAIL_EXEMPT_DOMAINS || "";
const IS_DEV = CALCOM_ENV === "development";
export const SINGLE_ORG_SLUG = process.env.NEXT_PUBLIC_SINGLE_ORG_SLUG;
/** https://app.cal.com */
@@ -39,21 +30,16 @@ export const WEBAPP_URL =
// OAuth needs to have HTTPS(which is not generally setup locally) and a valid tld(*.local isn't a valid tld)
// So for development purpose, we would stick to localhost only
export const WEBAPP_URL_FOR_OAUTH =
IS_PRODUCTION || IS_DEV ? WEBAPP_URL : "http://localhost:3000";
export const WEBAPP_URL_FOR_OAUTH = IS_PRODUCTION || IS_DEV ? WEBAPP_URL : "http://localhost:3000";
/** @deprecated use `WEBAPP_URL` */
export const BASE_URL = WEBAPP_URL;
export const WEBSITE_URL =
ensureProtocol(process.env.NEXT_PUBLIC_WEBSITE_URL) || "https://cal.com";
export const WEBSITE_URL = ensureProtocol(process.env.NEXT_PUBLIC_WEBSITE_URL) || "https://cal.com";
export const APP_NAME = process.env.NEXT_PUBLIC_APP_NAME || "Cal.com";
export const SUPPORT_MAIL_ADDRESS =
process.env.NEXT_PUBLIC_SUPPORT_MAIL_ADDRESS || "help@cal.com";
export const COMPANY_NAME =
process.env.NEXT_PUBLIC_COMPANY_NAME || "Cal.com, Inc.";
export const SUPPORT_MAIL_ADDRESS = process.env.NEXT_PUBLIC_SUPPORT_MAIL_ADDRESS || "help@cal.com";
export const COMPANY_NAME = process.env.NEXT_PUBLIC_COMPANY_NAME || "Cal.com, Inc.";
export const SENDER_ID = process.env.NEXT_PUBLIC_SENDER_ID || "Cal";
export const SENDER_NAME =
process.env.NEXT_PUBLIC_SENDGRID_SENDER_NAME || "Cal.com";
export const SENDER_NAME = process.env.NEXT_PUBLIC_SENDGRID_SENDER_NAME || "Cal.com";
export const EMAIL_FROM_NAME = process.env.EMAIL_FROM_NAME || APP_NAME;
// This is the URL from which all Cal Links and their assets are served.
@@ -82,11 +68,8 @@ export const CONSOLE_URL =
: `https://console.cal.com`;
const CAL_DOMAINS = [".cal.com", ".cal.dev", ".cal.eu", ".cal.qa"];
const WEBAPP_HOSTNAME = new URL(WEBAPP_URL).hostname;
export const IS_SELF_HOSTED = !CAL_DOMAINS.some((domain) =>
WEBAPP_HOSTNAME.endsWith(domain)
);
export const EMBED_LIB_URL =
process.env.NEXT_PUBLIC_EMBED_LIB_URL || `${WEBAPP_URL}/embed/embed.js`;
export const IS_SELF_HOSTED = !CAL_DOMAINS.some((domain) => WEBAPP_HOSTNAME.endsWith(domain));
export const EMBED_LIB_URL = process.env.NEXT_PUBLIC_EMBED_LIB_URL || `${WEBAPP_URL}/embed/embed.js`;
export const TRIAL_LIMIT_DAYS = 14;
export const MAX_SEATS_PER_TIME_SLOT = 1000;
@@ -96,26 +79,16 @@ export const MAX_EVENT_DURATION_MINUTES = 1440;
/** Minimum duration allowed for an event in minutes */
export const MIN_EVENT_DURATION_MINUTES = 1;
export const HOSTED_CAL_FEATURES =
process.env.NEXT_PUBLIC_HOSTED_CAL_FEATURES || !IS_SELF_HOSTED;
export const HOSTED_CAL_FEATURES = process.env.NEXT_PUBLIC_HOSTED_CAL_FEATURES || !IS_SELF_HOSTED;
export const PUBLIC_QUERY_RESERVATION_INTERVAL_SECONDS =
parseInt(
process.env.NEXT_PUBLIC_QUERY_RESERVATION_INTERVAL_SECONDS ?? "",
10
) || 30;
parseInt(process.env.NEXT_PUBLIC_QUERY_RESERVATION_INTERVAL_SECONDS ?? "", 10) || 30;
// Must be lower than PUBLIC_QUERY_RESERVATION_INTERVAL_SECONDS
export const PUBLIC_QUERY_RESERVATION_STALE_TIME_SECONDS =
parseInt(
process.env.NEXT_PUBLIC_QUERY_RESERVATION_STALE_TIME_SECONDS ?? "",
10
) || 20;
parseInt(process.env.NEXT_PUBLIC_QUERY_RESERVATION_STALE_TIME_SECONDS ?? "", 10) || 20;
export const PUBLIC_QUERY_AVAILABLE_SLOTS_INTERVAL_SECONDS =
parseInt(
process.env.NEXT_PUBLIC_QUERY_AVAILABLE_SLOTS_INTERVAL_SECONDS ?? "",
10
) || 5 * 60;
parseInt(process.env.NEXT_PUBLIC_QUERY_AVAILABLE_SLOTS_INTERVAL_SECONDS ?? "", 10) || 5 * 60;
export const PUBLIC_INVALIDATE_AVAILABLE_SLOTS_ON_BOOKING_FORM =
process.env.NEXT_PUBLIC_INVALIDATE_AVAILABLE_SLOTS_ON_BOOKING_FORM === "1";
@@ -124,8 +97,7 @@ export const PUBLIC_QUICK_AVAILABILITY_ROLLOUT =
parseInt(process.env.NEXT_PUBLIC_QUICK_AVAILABILITY_ROLLOUT ?? "", 10) || 0;
/** @deprecated use `WEBAPP_URL` */
export const NEXT_PUBLIC_BASE_URL =
process.env.NEXT_PUBLIC_WEBAPP_URL || `https://${process.env.VERCEL_URL}`;
export const NEXT_PUBLIC_BASE_URL = process.env.NEXT_PUBLIC_WEBAPP_URL || `https://${process.env.VERCEL_URL}`;
export const LOGO = "/calcom-logo-white-word.svg";
export const LOGO_DARK = "/cal-logo-word-black.svg";
export const LOGO_ICON = "/cal-com-icon-white.svg";
@@ -163,15 +135,10 @@ export const IS_TEAM_BILLING_ENABLED_CLIENT =
export const FULL_NAME_LENGTH_MAX_LIMIT = 50;
export const API_NAME_LENGTH_MAX_LIMIT = 80;
export const MINUTES_TO_BOOK = process.env.NEXT_PUBLIC_MINUTES_TO_BOOK || "5";
export const ENABLE_PROFILE_SWITCHER =
process.env.NEXT_PUBLIC_ENABLE_PROFILE_SWITCHER === "1";
export const ENABLE_PROFILE_SWITCHER = process.env.NEXT_PUBLIC_ENABLE_PROFILE_SWITCHER === "1";
// Needed for orgs
export const ALLOWED_HOSTNAMES = JSON.parse(
`[${process.env.ALLOWED_HOSTNAMES || ""}]`
) as string[];
export const RESERVED_SUBDOMAINS = JSON.parse(
`[${process.env.RESERVED_SUBDOMAINS || ""}]`
) as string[];
export const ALLOWED_HOSTNAMES = JSON.parse(`[${process.env.ALLOWED_HOSTNAMES || ""}]`) as string[];
export const RESERVED_SUBDOMAINS = JSON.parse(`[${process.env.RESERVED_SUBDOMAINS || ""}]`) as string[];
export const ORGANIZATION_SELF_SERVE_PRICE = parseFloat(
process.env.NEXT_PUBLIC_ORGANIZATIONS_SELF_SERVE_PRICE_NEW || "37"
@@ -182,19 +149,15 @@ export const IS_MAILHOG_ENABLED = process.env.E2E_TEST_MAILHOG_ENABLED === "1";
export const CALCOM_VERSION = process.env.NEXT_PUBLIC_CALCOM_VERSION as string;
export const APP_CREDENTIAL_SHARING_ENABLED =
!!process.env.CALCOM_CREDENTIAL_SYNC_SECRET &&
!!process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY;
!!process.env.CALCOM_CREDENTIAL_SYNC_SECRET && !!process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY;
export const CREDENTIAL_SYNC_SECRET = process.env.CALCOM_CREDENTIAL_SYNC_SECRET;
export const CREDENTIAL_SYNC_SECRET_HEADER_NAME =
process.env.CALCOM_CREDENTIAL_SYNC_HEADER_NAME ||
"calcom-credential-sync-secret";
process.env.CALCOM_CREDENTIAL_SYNC_HEADER_NAME || "calcom-credential-sync-secret";
export const CREDENTIAL_SYNC_ENDPOINT =
process.env.CALCOM_CREDENTIAL_SYNC_ENDPOINT;
export const CREDENTIAL_SYNC_ENDPOINT = process.env.CALCOM_CREDENTIAL_SYNC_ENDPOINT;
// Service Account Encryption Key for encrypting/decrypting service account keys
export const SERVICE_ACCOUNT_ENCRYPTION_KEY =
process.env.CALCOM_SERVICE_ACCOUNT_ENCRYPTION_KEY;
export const SERVICE_ACCOUNT_ENCRYPTION_KEY = process.env.CALCOM_SERVICE_ACCOUNT_ENCRYPTION_KEY;
export const DEFAULT_LIGHT_BRAND_COLOR = "#292929";
export const DEFAULT_DARK_BRAND_COLOR = "#fafafa";
@@ -210,9 +173,7 @@ export const MAX_NB_INVITES = 100;
export const URL_PROTOCOL_REGEX = /(^\w+:|^)\/\//;
export const IS_VISUAL_REGRESSION_TESTING = Boolean(
globalThis.window?.Meticulous?.isRunningAsTest
);
export const IS_VISUAL_REGRESSION_TESTING = Boolean(globalThis.window?.Meticulous?.isRunningAsTest);
export const BOOKER_NUMBER_OF_DAYS_TO_LOAD = parseInt(
process.env.NEXT_PUBLIC_BOOKER_NUMBER_OF_DAYS_TO_LOAD ?? "0",
@@ -220,20 +181,15 @@ export const BOOKER_NUMBER_OF_DAYS_TO_LOAD = parseInt(
);
export const CLOUDFLARE_SITE_ID = process.env.NEXT_PUBLIC_CLOUDFLARE_SITEKEY;
export const CLOUDFLARE_USE_TURNSTILE_IN_BOOKER =
process.env.NEXT_PUBLIC_CLOUDFLARE_USE_TURNSTILE_IN_BOOKER;
export const ORG_SELF_SERVE_ENABLED =
process.env.NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED === "1";
export const CLOUDFLARE_USE_TURNSTILE_IN_BOOKER = process.env.NEXT_PUBLIC_CLOUDFLARE_USE_TURNSTILE_IN_BOOKER;
export const ORG_SELF_SERVE_ENABLED = process.env.NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED === "1";
export const ORG_MINIMUM_PUBLISHED_TEAMS_SELF_SERVE = 0;
export const ORG_MINIMUM_PUBLISHED_TEAMS_SELF_SERVE_HELPER_DIALOGUE = 1;
export const CALCOM_PRIVATE_API_ROUTE =
process.env.CALCOM_PRIVATE_API_ROUTE || "https://goblin.cal.com";
export const CALCOM_PRIVATE_API_ROUTE = process.env.CALCOM_PRIVATE_API_ROUTE || "https://goblin.cal.com";
export const WEBSITE_PRIVACY_POLICY_URL =
process.env.NEXT_PUBLIC_WEBSITE_PRIVACY_POLICY_URL ||
"https://cal.com/privacy";
export const WEBSITE_TERMS_URL =
process.env.NEXT_PUBLIC_WEBSITE_TERMS_URL || "https://cal.com/terms";
process.env.NEXT_PUBLIC_WEBSITE_PRIVACY_POLICY_URL || "https://cal.com/privacy";
export const WEBSITE_TERMS_URL = process.env.NEXT_PUBLIC_WEBSITE_TERMS_URL || "https://cal.com/terms";
export const LINGO_DOT_DEV_API_KEY = process.env.LINGO_DOT_DEV_API_KEY;
/**
@@ -258,55 +214,36 @@ export const RECORDING_IN_PROGRESS_ICON = IS_PRODUCTION
? `${WEBAPP_URL}/stop-recording.svg`
: `https://app.cal.com/stop-recording.svg`;
export const SCOPE_USERINFO_PROFILE =
"https://www.googleapis.com/auth/userinfo.profile";
export const SCOPE_USERINFO_EMAIL =
"https://www.googleapis.com/auth/userinfo.email";
export const GOOGLE_OAUTH_SCOPES = [
SCOPE_USERINFO_PROFILE,
SCOPE_USERINFO_EMAIL,
];
export const SCOPE_USERINFO_PROFILE = "https://www.googleapis.com/auth/userinfo.profile";
export const SCOPE_USERINFO_EMAIL = "https://www.googleapis.com/auth/userinfo.email";
export const GOOGLE_OAUTH_SCOPES = [SCOPE_USERINFO_PROFILE, SCOPE_USERINFO_EMAIL];
export const GOOGLE_CALENDAR_SCOPES = [
"https://www.googleapis.com/auth/calendar.events",
"https://www.googleapis.com/auth/calendar.readonly",
];
export const DIRECTORY_IDS_TO_LOG =
process.env.DIRECTORY_IDS_TO_LOG?.split(",") || [];
export const SCANNING_WORKFLOW_STEPS = !!(
!IS_SELF_HOSTED && process.env.IFFY_API_KEY
);
export const DIRECTORY_IDS_TO_LOG = process.env.DIRECTORY_IDS_TO_LOG?.split(",") || [];
export const SCANNING_WORKFLOW_STEPS = !!(!IS_SELF_HOSTED && process.env.IFFY_API_KEY);
// Cloudflare URL Scanner - checks URLs for malicious content in workflows and event types
export const URL_SCANNING_ENABLED =
!!process.env.CLOUDFLARE_URL_SCANNER_API_TOKEN &&
!!process.env.CLOUDFLARE_ACCOUNT_ID;
!!process.env.CLOUDFLARE_URL_SCANNER_API_TOKEN && !!process.env.CLOUDFLARE_ACCOUNT_ID;
export const IS_DUB_REFERRALS_ENABLED =
!!process.env.NEXT_PUBLIC_DUB_PROGRAM_ID &&
process.env.NEXT_PUBLIC_DUB_PROGRAM_ID !== "";
!!process.env.NEXT_PUBLIC_DUB_PROGRAM_ID && process.env.NEXT_PUBLIC_DUB_PROGRAM_ID !== "";
export const CAL_VIDEO_MEETING_LINK_FOR_TESTING =
process.env.CAL_VIDEO_MEETING_LINK_FOR_TESTING;
export const CAL_VIDEO_MEETING_LINK_FOR_TESTING = process.env.CAL_VIDEO_MEETING_LINK_FOR_TESTING;
export const IS_SMS_CREDITS_ENABLED =
!!process.env.NEXT_PUBLIC_STRIPE_CREDITS_PRICE_ID ||
!!process.env.NEXT_PUBLIC_IS_E2E;
export const DATABASE_CHUNK_SIZE = parseInt(
process.env.DATABASE_CHUNK_SIZE || "25",
10
);
!!process.env.NEXT_PUBLIC_STRIPE_CREDITS_PRICE_ID || !!process.env.NEXT_PUBLIC_IS_E2E;
export const DATABASE_CHUNK_SIZE = parseInt(process.env.DATABASE_CHUNK_SIZE || "25", 10);
export const NEXTJS_CACHE_TTL = 3600; // 1 hour
export const DEFAULT_GROUP_ID = "default_group_id";
const _rawCalAiPrice =
process.env.NEXT_PUBLIC_CAL_AI_PHONE_NUMBER_MONTHLY_PRICE;
const _rawCalAiPrice = process.env.NEXT_PUBLIC_CAL_AI_PHONE_NUMBER_MONTHLY_PRICE;
export const CAL_AI_PHONE_NUMBER_MONTHLY_PRICE = (() => {
const parsed =
_rawCalAiPrice && _rawCalAiPrice.trim() !== ""
? Number(_rawCalAiPrice)
: NaN;
const parsed = _rawCalAiPrice && _rawCalAiPrice.trim() !== "" ? Number(_rawCalAiPrice) : NaN;
return Number.isFinite(parsed) ? parsed : 5;
})();
@@ -338,6 +275,4 @@ export const ORG_TRIAL_DAYS = process.env.STRIPE_ORG_TRIAL_DAYS
export const IS_API_V2_E2E = process.env.IS_E2E === "true";
export const ENABLE_ASYNC_TASKER =
process.env.ENABLE_ASYNC_TASKER === "true" &&
!process.env.NEXT_PUBLIC_IS_E2E &&
!IS_API_V2_E2E;
process.env.ENABLE_ASYNC_TASKER === "true" && !process.env.NEXT_PUBLIC_IS_E2E && !IS_API_V2_E2E;
+3 -1
View File
@@ -29,7 +29,9 @@ export const objectsToCsv = (data: Record<string, any>[]): string => {
// Create CSV rows
const csvRows = [
// Header row
headers.map((header) => sanitizeValue(header)).join(","),
headers
.map((header) => sanitizeValue(header))
.join(","),
// Data rows
...data.map((row) =>
headers
@@ -81,7 +81,7 @@ function handleDomainCreationError(error: {
domain?: string | null;
message?: string | null;
invalidToken?: boolean | null;
}){
}) {
// Vercel returns "forbidden" for various permission issues, not just domain ownership
if (error.code === "forbidden") {
const errorMessage =
@@ -130,7 +130,7 @@ function handleDomainDeletionError(error: {
domain?: string | null;
message?: string | null;
invalidToken?: boolean | null;
}){
}) {
if (error.code === "not_found") {
// Domain is already deleted
return true;
+4 -4
View File
@@ -26,18 +26,18 @@ export class ErrorWithCode extends Error {
/**
* Converts unknown error types to Error objects.
*
*
* @deprecated For server-side code, use `getServerErrorFromUnknown` from `@calcom/lib/server/getServerErrorFromUnknown` instead.
* This function should only be used in client-side or isomorphic code (React components, shared utilities).
*
*
* Use this function when:
* - You're in a React component that runs on the client
* - You're in shared/isomorphic code that cannot import server-only dependencies
* - You only need a basic Error object without HTTP status code mapping
*
*
* For server-side error handling (API routes, tRPC handlers, webhooks), use `getServerErrorFromUnknown`
* which provides proper HTTP status code mapping, error redaction, and handles Zod/Prisma/Stripe errors.
*
*
* @param cause - The unknown error to convert
* @returns An Error object with optional statusCode and code properties
*/
+1 -1
View File
@@ -92,7 +92,7 @@ export const getBookingWithResponses = <
select: BookingSelect & {
responses: true;
};
}>
}>,
>(
booking: T
) => {
-1
View File
@@ -48,4 +48,3 @@ export function isSafeUrlToLoadResourceFrom(urlString: string) {
return false;
}
}
+1 -1
View File
@@ -139,7 +139,7 @@ export function filterActiveLinks<
expiresAt?: Date | null;
maxUsageCount?: number | null;
usageCount?: number | null;
}
},
>(links: T[], timezone?: string | null): T[] {
return links.filter((link) => !isLinkExpired(link, timezone));
}
@@ -93,4 +93,3 @@ export function getGoogleCalendarClient(): GoogleCalendarClient {
}
return defaultClient;
}
@@ -2,7 +2,11 @@ import dayjs from "@calcom/dayjs";
import { HolidayRepository } from "@calcom/features/holidays/repositories/HolidayRepository";
import { GOOGLE_HOLIDAY_CALENDARS, HOLIDAY_CACHE_DAYS } from "./constants";
import { getGoogleCalendarClient, type GoogleCalendarClient, type GoogleCalendarHoliday } from "./GoogleCalendarClient";
import {
getGoogleCalendarClient,
type GoogleCalendarClient,
type GoogleCalendarHoliday,
} from "./GoogleCalendarClient";
export interface CachedHoliday {
id: string;
@@ -67,14 +71,16 @@ export class HolidayServiceCachingProxy {
const cached = await HolidayRepository.findManyCachedHolidays({ countryCode, year });
return cached.map((h): CachedHoliday => ({
id: h.id,
countryCode: h.countryCode,
eventId: h.eventId,
name: h.name,
date: h.date,
year: h.year,
}));
return cached.map(
(h): CachedHoliday => ({
id: h.id,
countryCode: h.countryCode,
eventId: h.eventId,
name: h.name,
date: h.date,
year: h.year,
})
);
}
async getHolidaysInRange(countryCode: string, startDate: Date, endDate: Date): Promise<CachedHoliday[]> {
@@ -94,14 +100,16 @@ export class HolidayServiceCachingProxy {
endDate,
});
return cached.map((h): CachedHoliday => ({
id: h.id,
countryCode: h.countryCode,
eventId: h.eventId,
name: h.name,
date: h.date,
year: h.year,
}));
return cached.map(
(h): CachedHoliday => ({
id: h.id,
countryCode: h.countryCode,
eventId: h.eventId,
name: h.name,
date: h.date,
year: h.year,
})
);
}
}
@@ -113,4 +121,3 @@ export function getHolidayServiceCachingProxy(): HolidayServiceCachingProxy {
}
return defaultProxy;
}
+8 -3
View File
@@ -13,7 +13,10 @@ export const GOOGLE_RELIGIOUS_HOLIDAY_CALENDARS: Record<string, { name: string;
HINDU: { name: "Hindu Holidays", calendarId: "en.hinduism#holiday@group.v.calendar.google.com" },
JEWISH: { name: "Jewish Holidays", calendarId: "en.judaism#holiday@group.v.calendar.google.com" },
MUSLIM: { name: "Muslim Holidays", calendarId: "en.islamic#holiday@group.v.calendar.google.com" },
ORTHODOX: { name: "Orthodox Holidays", calendarId: "en.orthodox_christianity#holiday@group.v.calendar.google.com" },
ORTHODOX: {
name: "Orthodox Holidays",
calendarId: "en.orthodox_christianity#holiday@group.v.calendar.google.com",
},
};
/**
@@ -90,7 +93,10 @@ export const GOOGLE_HOLIDAY_CALENDARS: Record<string, { name: string; calendarId
EE: { name: "Estonia", calendarId: "en.ee.official#holiday@group.v.calendar.google.com" },
SZ: { name: "Eswatini", calendarId: "en.sz.official#holiday@group.v.calendar.google.com" },
ET: { name: "Ethiopia", calendarId: "en.et.official#holiday@group.v.calendar.google.com" },
FK: { name: "Falkland Islands (Islas Malvinas)", calendarId: "en.fk.official#holiday@group.v.calendar.google.com" },
FK: {
name: "Falkland Islands (Islas Malvinas)",
calendarId: "en.fk.official#holiday@group.v.calendar.google.com",
},
FO: { name: "Faroe Islands", calendarId: "en.fo.official#holiday@group.v.calendar.google.com" },
FJ: { name: "Fiji", calendarId: "en.fj.official#holiday@group.v.calendar.google.com" },
FI: { name: "Finland", calendarId: "en.finnish.official#holiday@group.v.calendar.google.com" },
@@ -257,4 +263,3 @@ export const GOOGLE_HOLIDAY_CALENDARS: Record<string, { name: string; calendarId
ZM: { name: "Zambia", calendarId: "en.zm.official#holiday@group.v.calendar.google.com" },
ZW: { name: "Zimbabwe", calendarId: "en.zw.official#holiday@group.v.calendar.google.com" },
};
+4 -1
View File
@@ -31,7 +31,10 @@ const HOLIDAY_EMOJI_MAP: Array<{ keywords: string[]; emoji: string }> = [
{ keywords: ["revolution", "revolución"], emoji: "✊" },
{ keywords: ["unity", "unification", "einheit", "german unity"], emoji: "🤝" },
{ keywords: ["flag day", "día de la bandera"], emoji: "🚩" },
{ keywords: ["labor", "labour", "workers", "trabajo", "travail", "may day", "tag der arbeit"], emoji: "👷" },
{
keywords: ["labor", "labour", "workers", "trabajo", "travail", "may day", "tag der arbeit"],
emoji: "👷",
},
{ keywords: ["diwali", "deepavali", "dipavali"], emoji: "🪔" },
{ keywords: ["holi"], emoji: "🎨" },
{ keywords: ["dussehra", "dasara", "vijayadashami"], emoji: "🏹" },
@@ -5,7 +5,9 @@ import { describe, expect, it } from "vitest";
import { useCompatSearchParams } from "./useCompatSearchParams";
vi.mock("next/navigation", () => ({
ReadonlyURLSearchParams: vi.fn().mockImplementation(function(a) { return a; }),
ReadonlyURLSearchParams: vi.fn().mockImplementation(function (a) {
return a;
}),
}));
describe("useCompatSearchParams hook", () => {
@@ -21,4 +21,7 @@ function _getPeriodStartDatesBetween(
return dates;
}
export const getPeriodStartDatesBetween = withReporting(_getPeriodStartDatesBetween, "getPeriodStartDatesBetween");
export const getPeriodStartDatesBetween = withReporting(
_getPeriodStartDatesBetween,
"getPeriodStartDatesBetween"
);
+1 -1
View File
@@ -1,4 +1,4 @@
import fs from "node:fs"
import fs from "node:fs";
import logger from "./logger";
+1 -1
View File
@@ -6,4 +6,4 @@ export function caseInsensitive<T extends string | string[]>(
? stringOrStringArray.map((string) => string.toLowerCase())
: stringOrStringArray.toLowerCase()
) as T extends string[] ? string[] : string;
}
}
+1 -1
View File
@@ -38,7 +38,7 @@ export function rateLimiter() {
log.warn("Disabled because the UNKEY_ROOT_KEY environment variable was not found.");
warned = true;
}
return () => ({ success: true, limit: 10, remaining: 999, reset: 0 } as RatelimitResponse);
return () => ({ success: true, limit: 10, remaining: 999, reset: 0 }) as RatelimitResponse;
}
const timeout = {
fallback: { success: true, limit: 10, remaining: 999, reset: 0 },
+32 -29
View File
@@ -30,37 +30,40 @@ export function transformDateOverridesForAtom(
schedule: { availability: ScheduleOverride },
timeZone: string
) {
const acc = schedule.availability.reduce((acc, override) => {
// only if future date override
const currentUtcOffset = dayjs().tz(timeZone).utcOffset();
const currentTimeInTz = dayjs().utc().add(currentUtcOffset, "minute");
const acc = schedule.availability.reduce(
(acc, override) => {
// only if future date override
const currentUtcOffset = dayjs().tz(timeZone).utcOffset();
const currentTimeInTz = dayjs().utc().add(currentUtcOffset, "minute");
if (!override.date || dayjs(override.date).isBefore(currentTimeInTz, "day")) {
if (!override.date || dayjs(override.date).isBefore(currentTimeInTz, "day")) {
return acc;
}
const newValue = {
start: dayjs
.utc(override.date)
.hour(override.startTime.getUTCHours())
.minute(override.startTime.getUTCMinutes())
.toDate(),
end: dayjs
.utc(override.date)
.hour(override.endTime.getUTCHours())
.minute(override.endTime.getUTCMinutes())
.toDate(),
};
const dayRangeIndex = acc.findIndex(
// early return prevents override.date from ever being empty.
(item) => override.date && yyyymmdd(item.ranges[0].start) === yyyymmdd(override.date)
);
if (dayRangeIndex === -1) {
acc.push({ ranges: [newValue] });
return acc;
}
acc[dayRangeIndex].ranges.push(newValue);
return acc;
}
const newValue = {
start: dayjs
.utc(override.date)
.hour(override.startTime.getUTCHours())
.minute(override.startTime.getUTCMinutes())
.toDate(),
end: dayjs
.utc(override.date)
.hour(override.endTime.getUTCHours())
.minute(override.endTime.getUTCMinutes())
.toDate(),
};
const dayRangeIndex = acc.findIndex(
// early return prevents override.date from ever being empty.
(item) => override.date && yyyymmdd(item.ranges[0].start) === yyyymmdd(override.date)
);
if (dayRangeIndex === -1) {
acc.push({ ranges: [newValue] });
return acc;
}
acc[dayRangeIndex].ranges.push(newValue);
return acc;
}, [] as { ranges: TimeRange[] }[]);
},
[] as { ranges: TimeRange[] }[]
);
acc.sort((a, b) => {
const aTime = a.ranges?.[0]?.start?.getTime?.() ?? 0;
+4 -3
View File
@@ -138,9 +138,10 @@ const noOpSpan: TelemetrySpan = {
* Creates a console-logging span for local development when Sentry is not configured.
* Tracks attributes and logs them with duration when the span completes.
*/
function createConsoleSpan(
options: TelemetrySpanOptions
): { span: TelemetrySpan; logCompletion: (durationMs: number, error?: unknown) => void } {
function createConsoleSpan(options: TelemetrySpanOptions): {
span: TelemetrySpan;
logCompletion: (durationMs: number, error?: unknown) => void;
} {
const attributes: Record<string, string | number | boolean> = { ...options.attributes };
const span: TelemetrySpan = {
-1
View File
@@ -2,7 +2,6 @@ import { describe, it, expect } from "vitest";
import { hashEmail, Md5PiiHasher } from "./PiiHasher";
describe("PII Hasher Test Suite", () => {
const hasher = new Md5PiiHasher("test-salt");
it("can hash email addresses deterministically and preserve domain", async () => {
@@ -22,7 +22,10 @@ function isZodError(cause: unknown): cause is ZodError {
function isPrismaError(cause: unknown): cause is Prisma.PrismaClientKnownRequestError {
return (
cause instanceof Prisma.PrismaClientKnownRequestError ||
(cause instanceof Error && "code" in cause && typeof cause.code === "string" && cause.code.startsWith("P"))
(cause instanceof Error &&
"code" in cause &&
typeof cause.code === "string" &&
cause.code.startsWith("P"))
);
}
@@ -32,8 +35,8 @@ function parseZodErrorIssues(issues: ZodIssue[]): string {
i.code === "invalid_union"
? i.unionErrors.map((ue) => parseZodErrorIssues(ue.issues)).join("; ")
: i.code === "unrecognized_keys"
? i.message
: `${i.path.length ? `${i.code} in '${i.path}': ` : ""}${i.message}`
? i.message
: `${i.path.length ? `${i.code} in '${i.path}': ` : ""}${i.message}`
)
.join("; ");
}
+1 -1
View File
@@ -13,7 +13,7 @@ export function withSelectedCalendars<
},
TUser extends {
selectedCalendars: TCalendar[];
}
},
>(user: UserWithLegacySelectedCalendars<TCalendar, TUser>): UserWithSelectedCalendars<TCalendar, TUser> {
// We are renaming selectedCalendars to allSelectedCalendars to make it clear that it contains all the calendars including eventType calendars
const { selectedCalendars, ...restUser } = user;
+3 -7
View File
@@ -81,16 +81,12 @@ describe("validateUrlForSSRFSync", () => {
});
it("allows /api/avatar/{uuid}.png only", () => {
expect(validateUrlForSSRFSync("/api/avatar/ba0fa3a6-2aac-4032-8230-3789f5752e5a.png").isValid).toBe(
true
);
expect(validateUrlForSSRFSync("/api/avatar/ba0fa3a6-2aac-4032-8230-3789f5752e5a.png").isValid).toBe(true);
expect(validateUrlForSSRFSync("/api/avatar/any-value.png").isValid).toBe(true);
});
it("rejects /api/avatar/ path without .png extension", () => {
expect(validateUrlForSSRFSync("/api/avatar/ba0fa3a6-2aac-4032-8230-3789f5752e5a").isValid).toBe(
false
);
expect(validateUrlForSSRFSync("/api/avatar/ba0fa3a6-2aac-4032-8230-3789f5752e5a").isValid).toBe(false);
expect(validateUrlForSSRFSync("/api/avatar/foo.jpg").isValid).toBe(false);
});
@@ -213,4 +209,4 @@ describe("Self-hosted environment behavior", () => {
expect(validateSelfHosted("ftp://internal-server/file").isValid).toBe(false);
expect(validateSelfHosted("javascript:alert(1)").isValid).toBe(false);
});
});
});
+9 -29
View File
@@ -7,9 +7,7 @@ import { redactError } from "../redactError";
import type { ILogger } from "./types";
const isAsyncTaskerEnabled =
ENABLE_ASYNC_TASKER &&
process.env.TRIGGER_SECRET_KEY &&
process.env.TRIGGER_API_URL;
ENABLE_ASYNC_TASKER && process.env.TRIGGER_SECRET_KEY && process.env.TRIGGER_API_URL;
export abstract class Tasker<T> {
protected readonly asyncTasker: T;
@@ -24,10 +22,7 @@ export abstract class Tasker<T> {
this.logger = dependencies.logger;
if (!isAsyncTaskerEnabled) {
if (
ENABLE_ASYNC_TASKER &&
(!process.env.TRIGGER_SECRET_KEY || !process.env.TRIGGER_API_URL)
) {
if (ENABLE_ASYNC_TASKER && (!process.env.TRIGGER_SECRET_KEY || !process.env.TRIGGER_API_URL)) {
this.logger.info(
"Missing env variables TRIGGER_SECRET_KEY or TRIGGER_API_URL, falling back to Sync tasker."
);
@@ -41,18 +36,14 @@ export abstract class Tasker<T> {
});
}
this.asyncTasker = isAsyncTaskerEnabled
? dependencies.asyncTasker
: dependencies.syncTasker;
this.asyncTasker = isAsyncTaskerEnabled ? dependencies.asyncTasker : dependencies.syncTasker;
this.syncTasker = dependencies.syncTasker;
}
public async dispatch<K extends keyof T>(
taskName: K,
...args: T[K] extends (...args: any[]) => any ? Parameters<T[K]> : never
): Promise<
T[K] extends (...args: any[]) => any ? Awaited<ReturnType<T[K]>> : never
> {
): Promise<T[K] extends (...args: any[]) => any ? Awaited<ReturnType<T[K]>> : never> {
this.logger.info(`Safely Dispatching task '${String(taskName)}'`, { args });
return this._safeDispatch(taskName, ...args);
}
@@ -60,14 +51,10 @@ export abstract class Tasker<T> {
private async _safeDispatch<K extends keyof T>(
taskName: K,
...args: T[K] extends (...args: any[]) => any ? Parameters<T[K]> : never
): Promise<
T[K] extends (...args: any[]) => any ? Awaited<ReturnType<T[K]>> : never
> {
): Promise<T[K] extends (...args: any[]) => any ? Awaited<ReturnType<T[K]>> : never> {
try {
this.logger.info(
`${isAsyncTaskerEnabled ? "AsyncTasker" : "SyncTasker"} '${String(
taskName
)}' dispatched.`
`${isAsyncTaskerEnabled ? "AsyncTasker" : "SyncTasker"} '${String(taskName)}' dispatched.`
);
const method = this.asyncTasker[taskName] as (...args: any[]) => any;
return await method.apply(this.asyncTasker, args);
@@ -85,20 +72,13 @@ export abstract class Tasker<T> {
throw err;
}
this.logger.warn(
`Trying again with SyncTasker for '${String(taskName)}'.`
);
this.logger.warn(`Trying again with SyncTasker for '${String(taskName)}'.`);
try {
const fallbackMethod = this.syncTasker[taskName] as (
...args: any[]
) => any;
const fallbackMethod = this.syncTasker[taskName] as (...args: any[]) => any;
return await fallbackMethod.apply(this.syncTasker, args);
} catch (err) {
this.logger.error(
`SyncTasker failed for '${String(taskName)}'.`,
this.getErrorDetails(err)
);
this.logger.error(`SyncTasker failed for '${String(taskName)}'.`, this.getErrorDetails(err));
throw err;
}
}
+14 -2
View File
@@ -269,7 +269,13 @@ describe("DistributedTracing", () => {
tracing.getTracingLogger(context);
expect(mockLogger.getSubLogger).toHaveBeenCalledWith({
prefix: ["distributed-trace", "trace:trace_123", "span:span_456", "op:test_operation", "userId:user_789"],
prefix: [
"distributed-trace",
"trace:trace_123",
"span:span_456",
"op:test_operation",
"userId:user_789",
],
});
});
@@ -287,7 +293,13 @@ describe("DistributedTracing", () => {
tracing.getTracingLogger(context);
expect(mockLogger.getSubLogger).toHaveBeenCalledWith({
prefix: ["distributed-trace", "trace:trace_123", "span:span_456", "op:test_operation", "userId:user_789"],
prefix: [
"distributed-trace",
"trace:trace_123",
"span:span_456",
"op:test_operation",
"userId:user_789",
],
});
});
});
+4 -1
View File
@@ -14,7 +14,10 @@ export interface IdGenerator {
}
export class DistributedTracing {
constructor(private idGenerator: IdGenerator, private loggerInstance: Logger<unknown>) {}
constructor(
private idGenerator: IdGenerator,
private loggerInstance: Logger<unknown>
) {}
createTrace(operation: string, context?: Partial<TraceContext>): TraceContext {
const { traceId, spanId, meta, ...otherContext } = context || {};
-1
View File
@@ -4,4 +4,3 @@ export {
type GoogleAdsTrackingData,
type LinkedInAdsTrackingData,
} from "./server";
+4 -12
View File
@@ -1,12 +1,8 @@
import { createHmac } from "node:crypto";
// 262992 minutes is 6 months
export function generateVideoToken(
recordingId: string,
expiresInMinutes = 262992
) {
const secret =
process.env.CAL_VIDEO_RECORDING_TOKEN_SECRET || "default-secret-change-me";
export function generateVideoToken(recordingId: string, expiresInMinutes = 262992) {
const secret = process.env.CAL_VIDEO_RECORDING_TOKEN_SECRET || "default-secret-change-me";
const expires = Date.now() + expiresInMinutes * 60 * 1000;
const payload = `${recordingId}:${expires}`;
@@ -21,9 +17,7 @@ export function verifyVideoToken(token: string): {
} {
try {
const [recordingId, expires, receivedHmac] = token.split(":");
const secret =
process.env.CAL_VIDEO_RECORDING_TOKEN_SECRET ||
"default-secret-change-me";
const secret = process.env.CAL_VIDEO_RECORDING_TOKEN_SECRET || "default-secret-change-me";
if (Date.now() > parseInt(expires, 10)) {
return { valid: false };
@@ -31,9 +25,7 @@ export function verifyVideoToken(token: string): {
// Verify HMAC
const payload = `${recordingId}:${expires}`;
const expectedHmac = createHmac("sha256", secret)
.update(payload)
.digest("hex");
const expectedHmac = createHmac("sha256", secret).update(payload).digest("hex");
if (receivedHmac !== expectedHmac) {
return { valid: false };
-1
View File
@@ -12,7 +12,6 @@ const validateSsrfUrl = (url: string | null | undefined): boolean => {
return validateUrlForSSRFSync(url).isValid;
};
/**
* Zod schema for validating user-provided URLs before server-side fetching
* Applies synchronous SSRF checks only (no DNS resolution)
-1
View File
@@ -63,7 +63,6 @@ describe("excludeOrRequireEmailSchema", () => {
});
describe("invalid inputs", () => {
it("rejects invalid TLD lengths", () => {
expect(parse("example.c").success).toBe(false);
});
+17 -11
View File
@@ -357,9 +357,14 @@ export const stringOrNumber = z.union([
export const requiredCustomInputSchema = z.union([
// string must be given & nonempty
z.string().trim().min(1),
z
.string()
.trim()
.min(1),
// boolean must be true if set.
z.boolean().refine((v) => v === true),
z
.boolean()
.refine((v) => v === true),
]);
const PlatformClientParamsSchema = z.object({
@@ -643,7 +648,7 @@ export function denullishShape<
UnknownKeys extends UnknownKeysParam = "strip",
Catchall extends ZodTypeAny = ZodTypeAny,
Output = objectOutputType<T, Catchall>,
Input = objectInputType<T, Catchall>
Input = objectInputType<T, Catchall>,
>(
obj: ZodObject<T, UnknownKeys, Catchall, Output, Input>
): ZodObject<ZodDenullishShape<T>, UnknownKeys, Catchall> {
@@ -675,13 +680,14 @@ export const entries = <O extends Record<string, unknown>>(
/**
* Returns a type with all readonly notations removed (traverses recursively on an object)
*/
type DeepWriteable<T> = T extends Readonly<{
-readonly [K in keyof T]: T[K];
}>
? {
-readonly [K in keyof T]: DeepWriteable<T[K]>;
}
: T; /* Make it work with readonly types (this is not strictly necessary) */
type DeepWriteable<T> =
T extends Readonly<{
-readonly [K in keyof T]: T[K];
}>
? {
-readonly [K in keyof T]: DeepWriteable<T[K]>;
}
: T; /* Make it work with readonly types (this is not strictly necessary) */
type FromEntries<T> = T extends [infer Keys, unknown][]
? { [K in Keys & PropertyKey]: Extract<T[number], [K, unknown]>[1] }
@@ -694,7 +700,7 @@ type FromEntries<T> = T extends [infer Keys, unknown][]
* @see https://github.com/3x071c/lsg-remix/blob/e2a9592ba3ec5103556f2cf307c32f08aeaee32d/app/lib/util/fromEntries.ts
*/
export const fromEntries = <
E extends [PropertyKey, unknown][] | ReadonlyArray<readonly [PropertyKey, unknown]>
E extends [PropertyKey, unknown][] | ReadonlyArray<readonly [PropertyKey, unknown]>,
>(
entries: E
): FromEntries<DeepWriteable<E>> => {
+1 -1
View File
@@ -15,7 +15,7 @@ export default class EventCancelledSMS extends SMSManager {
const messageText = `${t("hey_there")} ${attendee.name}, ${t("event_request_cancelled")}\n\n${t(
"event_cancelled_subject",
{
title: typeof this.calEvent.title === 'string' ? this.calEvent.title : 'Untitled Event',
title: typeof this.calEvent.title === "string" ? this.calEvent.title : "Untitled Event",
date: this.getFormattedDate(attendee.timeZone, attendee.language.locale),
interpolation: { escapeValue: false },
}
+2 -2
View File
@@ -33,8 +33,8 @@ const handleSendingSMS = async ({
identifier: teamId
? `handleSendingSMS:team-${teamId}`
: organizerUserId
? `handleSendingSMS:org-user-${organizerUserId}`
: `handleSendingSMS:user-${piiHasher.hash(reminderPhone)}`,
? `handleSendingSMS:org-user-${organizerUserId}`
: `handleSendingSMS:user-${piiHasher.hash(reminderPhone)}`,
rateLimitingType: "sms",
});