Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/web/pages/api/cron/bookingReminder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});

const attendeesList = await Promise.all(attendeesListPromises);

const selectedDestinationCalendar = booking.destinationCalendar || user.destinationCalendar;
const evt: CalendarEvent = {
type: booking.title,
title: booking.title,
Expand All @@ -127,7 +127,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
attendees: attendeesList,
uid: booking.uid,
recurringEvent: parseRecurringEvent(booking.eventType?.recurringEvent),
destinationCalendar: booking.destinationCalendar || user.destinationCalendar,
destinationCalendar: selectedDestinationCalendar ? [selectedDestinationCalendar] : [],
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MINOR — Data exposure (confidence: 73%)

The cron endpoint now wraps destinationCalendar in an array ([selectedDestinationCalendar]) or empty array. The cron endpoint lacks verification that the request is from an authorized source (the existing API key check at line ~6 uses a weak comparison). While this is pre-existing, the change to wrap calendar data in arrays means any SSRF or unauthorized access to this cron endpoint now has access to structured multi-calendar data.

Evidence:

  • Static scan flagged weak crypto at lines 6 and 15 of bookingReminder.ts
  • The cron endpoint processes sensitive calendar credential IDs in the destinationCalendar array
  • An unauthorized caller could trigger reminder emails and observe calendar metadata

Agent: security

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [Grapple PR] Suggested fix — security agent (Larger fix (32 lines, 1 file) — review recommended)

The cron endpoint now wraps destinationCalendar in an array ([selectedDestinationCalendar]) or empty array. The cron endpoint lacks verification that the request is from an authorized source (the existing API key check at line ~6 uses a weak comparison). While this is pre-existing, the change to wrap calendar data in arrays means any SSRF or unauthorized access to this cron endpoint now has access to structured multi-calendar data.

--- a/apps/web/pages/api/cron/bookingReminder.ts
+++ b/apps/web/pages/api/cron/bookingReminder.ts
@@ -1,5 +1,6 @@
+import crypto from "crypto";
+
 import type { NextApiRequest, NextApiResponse } from "next";
 
 import dayjs from "@calcom/dayjs";
 import { sendOrganizerRequestReminderEmail } from "@calcom/emails";
 import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
@@ -8,18 +9,30 @@ import { getTranslation } from "@calcom/lib/server/i18n";
 import prisma, { bookingMinimalSelect } from "@calcom/prisma";
 import { BookingStatus, ReminderType } from "@calcom/prisma/enums";
 import type { CalendarEvent } from "@calcom/types/Calendar";
 
 export default async function handler(req: NextApiRequest, res: NextApiResponse) {
-  const apiKey = req.headers.authorization || req.query.apiKey;
-  if (process.env.CRON_API_KEY !== apiKey) {
-    res.status(401).json({ message: "Not authenticated" });
-    return;
+  if (req.method !== "POST") {
+    res.status(405).json({ message: "Method not allowed" });
+    return;
   }
 
-  if (req.method !== "POST") {
-    res.status(405).json({ message: "Not authenticated" });
+  const expectedApiKey = process.env.CRON_API_KEY;
+  if (!expectedApiKey) {
+    // No CRON_API_KEY configured — refuse all requests to avoid open access
+    res.status(500).json({ message: "Server misconfiguration" });
     return;
   }
 
+  const providedApiKey =
+    (req.headers.authorization ?? "").replace(/^Bearer\s+/i, "") ||
+    (typeof req.query.apiKey === "string" ? req.query.apiKey : "");
+
+  // Use constant-time comparison to prevent timing-based enumeration of the API key.
+  // A simple `===` comparison leaks timing information proportional to the length of
+  // the matching prefix, which static analysis (lines 6 & 15) flagged as weak crypto.
+  const expected = Buffer.from(expectedApiKey, "utf8");
+  const provided = Buffer.from(providedApiKey, "utf8");
+  const keysMatch =
+    expected.length === provided.length &&
+    crypto.timingSafeEqual(expected, provided);
+
+  if (!keysMatch) {
+    res.status(401).json({ message: "Not authenticated" });
+    return;
+  }

🤖 Grapple PR auto-fix • minor • Review this diff before applying


await sendOrganizerRequestReminderEmail(evt);
Expand Down
2 changes: 1 addition & 1 deletion apps/web/playwright/webhook.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ test.describe("BOOKING_REJECTED", async () => {
},
],
location: "[redacted/dynamic]",
destinationCalendar: null,
destinationCalendar: [],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MINOR — Webhook Contract Change (confidence: 89%)

The webhook payload contract changed destinationCalendar from null to [] for bookings without a destination calendar. This is a breaking change for webhook consumers that check if (payload.destinationCalendar === null) or similar null-specific checks. While the e2e test was updated, external webhook consumers are not controlled by this codebase.

Evidence:

  • Line 249: changed from destinationCalendar: null to destinationCalendar: []
  • External systems consuming webhooks may rely on the null value to indicate no destination calendar

Agent: architecture

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Grapple PR] Auto-fix — architecture agent (Small fix (8 lines, 1 file))

The webhook payload contract changed destinationCalendar from null to [] for bookings without a destination calendar. This is a breaking change for webhook consumers that check if (payload.destinationCalendar === null) or similar null-specific checks. While the e2e test was updated, external webhook consumers are not controlled by this codebase.

Suggested change
destinationCalendar: [],
// BREAKING CHANGE (introduced with collective event type multi-host support):
// `destinationCalendar` was previously `null` when no destination calendar was set.
// It is now always an array (`[]` when empty) to uniformly support multiple host
// calendars in collective event types. External webhook consumers that check
// `payload.destinationCalendar === null` must be updated to also handle `[]`.
// See: https://cal.com/docs/webhooks for versioning guidance.
destinationCalendar: [],

🤖 Grapple PR auto-fix • minor • confidence: 89%

// hideCalendarNotes: false,
requiresConfirmation: "[redacted/dynamic]",
eventTypeId: "[redacted/dynamic]",
Expand Down
33 changes: 22 additions & 11 deletions packages/app-store/googlecalendar/lib/CalendarService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export default class GoogleCalendarService implements Calendar {
};
};

async createEvent(calEventRaw: CalendarEvent): Promise<NewCalendarEventType> {
async createEvent(calEventRaw: CalendarEvent, credentialId: number): Promise<NewCalendarEventType> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 MAJOR — Missing Credential for Google Calendar createEvent (confidence: 100%)

The createEvent method signature now requires a credentialId parameter, but the Calendar interface in Calendar.d.ts declares createEvent(event: CalendarEvent, credentialId: number). Calendar services that don't use credentialId (Lark, Office365, CalDAV/BaseCalendarService) don't accept the second parameter in their implementations. While TypeScript may allow extra unused params, Lark's createEvent signature on line 125 does not include it, creating an interface mismatch.

Evidence:

  • packages/types/Calendar.d.ts line 221: createEvent(event: CalendarEvent, credentialId: number): Promise;
  • packages/app-store/larkcalendar/lib/CalendarService.ts line 125: async createEvent(event: CalendarEvent) — missing credentialId param
  • packages/app-store/office365calendar/lib/CalendarService.ts line 72: async createEvent(event: CalendarEvent) — missing credentialId param
  • packages/lib/CalendarService.ts (BaseCalendarService) also doesn't accept credentialId

Agent: logic

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MINOR — Code Patterns (confidence: 93%)

Method signature changed to add credentialId: number parameter, but this breaks the Calendar interface contract. The interface definition in packages/types/Calendar.d.ts shows only createEvent(event: CalendarEvent, credentialId: number) but other calendar service implementations (Lark, Office365) do not yet accept this parameter.

Evidence:

  • Google Calendar service adds credentialId parameter at line 87
  • Lark and Office365 calendar services (lines in their respective files) do not have this parameter in their createEvent methods
  • Creates inconsistency across calendar service implementations

Agent: style

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Grapple PR] Auto-fix — style agent (Small fix (2 lines, 1 file))

Method signature changed to add credentialId: number parameter, but this breaks the Calendar interface contract. The interface definition in packages/types/Calendar.d.ts shows only createEvent(event: CalendarEvent, credentialId: number) but other calendar service implementations (Lark, Office365) do not yet accept this parameter.

Suggested change
async createEvent(calEventRaw: CalendarEvent, credentialId: number): Promise<NewCalendarEventType> {
async createEvent(event: CalendarEvent, _credentialId?: number): Promise<NewCalendarEventType> {

🤖 Grapple PR auto-fix • minor • confidence: 93%

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [Grapple PR] Suggested fix — logic agent (Larger fix (5 lines, 2 files) — review recommended)

The createEvent method signature now requires a credentialId parameter, but the Calendar interface in Calendar.d.ts declares createEvent(event: CalendarEvent, credentialId: number). Calendar services that don't use credentialId (Lark, Office365, CalDAV/BaseCalendarService) don't accept the second parameter in their implementations. While TypeScript may allow extra unused params, Lark's createEvent signature on line 125 does not include it, creating an interface mismatch.

--- a/packages/app-store/larkcalendar/lib/CalendarService.ts
+++ b/packages/app-store/larkcalendar/lib/CalendarService.ts
@@ -122,7 +122,7 @@ export default class LarkCalendarService implements Calendar {
     };
   }
 
-  async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
+  async createEvent(event: CalendarEvent, credentialId: number): Promise<NewCalendarEventType> {
     let eventId = "";
     let eventRespData;
     const mainHostDestinationCalendar = event.destinationCalendar
--- a/packages/app-store/office365calendar/lib/CalendarService.ts
+++ b/packages/app-store/office365calendar/lib/CalendarService.ts
@@ -69,7 +69,7 @@ export default class Office365CalendarService implements Calendar {
     };
   }
 
-  async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
+  async createEvent(event: CalendarEvent, credentialId: number): Promise<NewCalendarEventType> {
     const calendarId = event.destinationCalendar?.find(
       (cal) => cal.integration === "office365_calendar"
     )?.externalId;
--- a/packages/lib/CalendarService.ts
+++ b/packages/lib/CalendarService.ts
@@ -1,6 +1,7 @@
 import type {
   Calendar,
   CalendarEvent,
+  NewCalendarEventType,
 } from "@calcom/types/Calendar";
 
 // Ensure any abstract/base createEvent signature matches the interface

🤖 Grapple PR auto-fix • major • Review this diff before applying

const eventAttendees = calEventRaw.attendees.map(({ id: _id, ...rest }) => ({
...rest,
responseStatus: "accepted",
Expand All @@ -97,6 +97,10 @@ export default class GoogleCalendarService implements Calendar {
responseStatus: "accepted",
})) || [];
return new Promise(async (resolve, reject) => {
const [mainHostDestinationCalendar] =
calEventRaw?.destinationCalendar && calEventRaw?.destinationCalendar.length > 0
? calEventRaw.destinationCalendar
: [];
const myGoogleAuth = await this.auth.getToken();
const payload: calendar_v3.Schema$Event = {
summary: calEventRaw.title,
Expand All @@ -115,8 +119,8 @@ export default class GoogleCalendarService implements Calendar {
id: String(calEventRaw.organizer.id),
responseStatus: "accepted",
organizer: true,
email: calEventRaw.destinationCalendar?.externalId
? calEventRaw.destinationCalendar.externalId
email: mainHostDestinationCalendar?.externalId
? mainHostDestinationCalendar.externalId
: calEventRaw.organizer.email,
},
...eventAttendees,
Expand All @@ -138,13 +142,16 @@ export default class GoogleCalendarService implements Calendar {
const calendar = google.calendar({
version: "v3",
});
const selectedCalendar = calEventRaw.destinationCalendar?.externalId
? calEventRaw.destinationCalendar.externalId
: "primary";
// Find in calEventRaw.destinationCalendar the one with the same credentialId

const selectedCalendar = calEventRaw.destinationCalendar?.find(
(cal) => cal.credentialId === credentialId
)?.externalId;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 INFO — Code Organization (confidence: 86%)

Incomplete TODO comment added during refactoring. Lines 146-147 contain a TODO comment 'Find in calEventRaw.destinationCalendar the one with the same credentialId' but the implementation that follows appears to be the actual solution, making the TODO misleading.

Evidence:

  • Line 146-147 has comment: // Find in calEventRaw.destinationCalendar the one with the same credentialId
  • Lines 148-150 immediately implement exactly that: calEventRaw.destinationCalendar?.find((cal) => cal.credentialId === credentialId)
  • The TODO appears to be developer notes that were accidentally left in

Agent: style

calendar.events.insert(
{
auth: myGoogleAuth,
calendarId: selectedCalendar,
calendarId: selectedCalendar || "primary",
requestBody: payload,
conferenceDataVersion: 1,
sendUpdates: "none",
Expand Down Expand Up @@ -188,6 +195,8 @@ export default class GoogleCalendarService implements Calendar {

async updateEvent(uid: string, event: CalendarEvent, externalCalendarId: string): Promise<any> {
return new Promise(async (resolve, reject) => {
const [mainHostDestinationCalendar] =
event?.destinationCalendar && event?.destinationCalendar.length > 0 ? event.destinationCalendar : [];
const myGoogleAuth = await this.auth.getToken();
const eventAttendees = event.attendees.map(({ ...rest }) => ({
...rest,
Expand Down Expand Up @@ -216,8 +225,8 @@ export default class GoogleCalendarService implements Calendar {
id: String(event.organizer.id),
organizer: true,
responseStatus: "accepted",
email: event.destinationCalendar?.externalId
? event.destinationCalendar.externalId
email: mainHostDestinationCalendar?.externalId
? mainHostDestinationCalendar.externalId
: event.organizer.email,
},
...(eventAttendees as any),
Expand All @@ -244,7 +253,7 @@ export default class GoogleCalendarService implements Calendar {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 CRITICAL — Logic Error / Silent Data Loss (confidence: 100%)

In updateEvent, the fallback logic for selectedCalendar is logically broken. When externalCalendarId is falsy, the code searches for a destinationCalendar entry matching externalCalendarId (which is falsy), meaning find() will never match and selectedCalendar will be undefined. The same bug exists in deleteEvent at line 314-316.

Evidence:

  • Line 253: event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId — externalCalendarId is falsy here because the ternary only reaches this branch when externalCalendarId is falsy
  • Line 314-316: identical pattern in deleteEvent — event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId where externalCalendarId is again falsy
  • This means updates and deletes for events without an externalCalendarId will target undefined/defaultCalendarId, potentially operating on the wrong calendar or failing silently

Agent: architecture

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [Grapple PR] Suggested fix — architecture agent (Small fix (4 lines, 1 file))

In updateEvent, the fallback logic for selectedCalendar is logically broken. When externalCalendarId is falsy, the code searches for a destinationCalendar entry matching externalCalendarId (which is falsy), meaning find() will never match and selectedCalendar will be undefined. The same bug exists in deleteEvent at line 314-316.

--- a/packages/app-store/googlecalendar/lib/CalendarService.ts
+++ b/packages/app-store/googlecalendar/lib/CalendarService.ts
@@ -253,7 +253,7 @@ export default class GoogleCalendarService implements Calendar {
 
       const selectedCalendar = externalCalendarId
         ? externalCalendarId
-        : event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId;
+        : mainHostDestinationCalendar?.externalId;
 
       calendar.events.update(
         {
@@ -312,7 +312,7 @@ export default class GoogleCalendarService implements Calendar {
       const defaultCalendarId = "primary";
       const calendarId = externalCalendarId
         ? externalCalendarId
-        : event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId;
+        : mainHostDestinationCalendar?.externalId;
 
       calendar.events.delete(
         {

🤖 Grapple PR auto-fix • critical • Review this diff before applying

const selectedCalendar = externalCalendarId
? externalCalendarId

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 CRITICAL — Logic Bug - Dead Code / Always Falsy (confidence: 100%)

In updateEvent, the fallback expression event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId is used when externalCalendarId is falsy. But since the ternary condition already checks externalCalendarId is truthy, the else branch executes only when externalCalendarId is falsy/undefined. The .find() then searches for cal.externalId === undefined, which will almost never match a real calendar, resulting in selectedCalendar being undefined. This means updates will fail or go to the wrong calendar.

Evidence:

  • Line 253-255: const selectedCalendar = externalCalendarId ? externalCalendarId : event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId;
  • When externalCalendarId is falsy, the find() searches for cal.externalId === undefined/null, which won't match any real calendar
  • The original code was event.destinationCalendar?.externalId which would fall back to the single destination calendar's externalId

Agent: logic

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [Grapple PR] Suggested fix — logic agent (Small fix (4 lines, 1 file))

In updateEvent, the fallback expression event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId is used when externalCalendarId is falsy. But since the ternary condition already checks externalCalendarId is truthy, the else branch executes only when externalCalendarId is falsy/undefined. The .find() then searches for cal.externalId === undefined, which will almost never match a real calendar, resulting in selectedCalendar being undefined. This means updates will fail or go to the wrong calendar.

--- a/packages/app-store/googlecalendar/lib/CalendarService.ts
+++ b/packages/app-store/googlecalendar/lib/CalendarService.ts
@@ -253,7 +253,7 @@ export default class GoogleCalendarService implements Calendar {
 
       const selectedCalendar = externalCalendarId
         ? externalCalendarId
-        : event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId;
+        : mainHostDestinationCalendar?.externalId;
 
       calendar.events.update(
         {
@@ -312,7 +312,7 @@ export default class GoogleCalendarService implements Calendar {
 
       const calendarId = externalCalendarId
         ? externalCalendarId
-        : event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId;
+        : mainHostDestinationCalendar?.externalId;
 
       calendar.events.delete(
         {

🤖 Grapple PR auto-fix • critical • Review this diff before applying

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 MAJOR — Logic Error — Tautological Condition Breaks Calendar Fallback (confidence: 100%)

In updateEvent, when externalCalendarId is falsy, the fallback is event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId. Since externalCalendarId is falsy at this point, find searches for a calendar with externalId === undefined, which will never match. The same tautological pattern exists in deleteEvent. This means the fallback calendar resolution is always broken, causing events to be updated/deleted on an undefined calendar.

Evidence:

  • Line 253: const selectedCalendar = externalCalendarId ? externalCalendarId : event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId
  • When externalCalendarId is falsy, cal.externalId === externalCalendarId compares against the falsy value itself — this cannot find a valid calendar
  • Same pattern in deleteEvent around line 312-314
  • The original code was event.destinationCalendar?.externalId which would correctly use the first destination calendar; the new code breaks this fallback

Agent: security

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [Grapple PR] Suggested fix — security agent (Small fix (4 lines, 1 file))

In updateEvent, when externalCalendarId is falsy, the fallback is event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId. Since externalCalendarId is falsy at this point, find searches for a calendar with externalId === undefined, which will never match. The same tautological pattern exists in deleteEvent. This means the fallback calendar resolution is always broken, causing events to be updated/deleted on an undefined calendar.

--- a/packages/app-store/googlecalendar/lib/CalendarService.ts
+++ b/packages/app-store/googlecalendar/lib/CalendarService.ts
@@ -253,7 +253,7 @@ export default class GoogleCalendarService implements Calendar {
 
       const selectedCalendar = externalCalendarId
         ? externalCalendarId
-        : event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId;
+        : mainHostDestinationCalendar?.externalId;
 
       calendar.events.update(
         {
@@ -312,7 +312,7 @@ export default class GoogleCalendarService implements Calendar {
       const defaultCalendarId = "primary";
       const calendarId = externalCalendarId
         ? externalCalendarId
-        : event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId;
+        : mainHostDestinationCalendar?.externalId;
 
       calendar.events.delete(
         {

🤖 Grapple PR auto-fix • major • Review this diff before applying

: event.destinationCalendar?.externalId;
: event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MINOR — Unnecessary work (confidence: 100%)

In updateEvent, the selectedCalendar fallback logic is a no-op: when externalCalendarId is falsy, it calls .find(cal => cal.externalId === externalCalendarId) where externalCalendarId is also falsy/null, so the find will always return undefined, making the fallback always resolve to undefined.

Evidence:

  • const selectedCalendar = externalCalendarId ? externalCalendarId : event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId;
  • When externalCalendarId is falsy, the .find compares cal.externalId === externalCalendarId (falsy), which will never match a real calendar ID.
  • The result is always undefined in the fallback branch, effectively making the fallback dead code.

Agent: performance

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [Grapple PR] Suggested fix — performance agent (Small fix (4 lines, 1 file))

In updateEvent, the selectedCalendar fallback logic is a no-op: when externalCalendarId is falsy, it calls .find(cal => cal.externalId === externalCalendarId) where externalCalendarId is also falsy/null, so the find will always return undefined, making the fallback always resolve to undefined.

--- a/packages/app-store/googlecalendar/lib/CalendarService.ts
+++ b/packages/app-store/googlecalendar/lib/CalendarService.ts
@@ -253,7 +253,7 @@ export default class GoogleCalendarService implements Calendar {
 
       const selectedCalendar = externalCalendarId
         ? externalCalendarId
-        : event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId;
+        : mainHostDestinationCalendar?.externalId;
 
       calendar.events.update(
         {
@@ -312,7 +312,7 @@ export default class GoogleCalendarService implements Calendar {
 
       const calendarId = externalCalendarId
         ? externalCalendarId
-        : event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId;
+        : mainHostDestinationCalendar?.externalId;
 
       calendar.events.delete(
         {

🤖 Grapple PR auto-fix • minor • Review this diff before applying


Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 MAJOR — Unnecessary work (confidence: 100%)

Dead-code logic in updateEvent: the selectedCalendar fallback uses Array.find to search by externalCalendarId but the condition cal.externalId === externalCalendarId will always be true for the matched element and is used only when externalCalendarId is falsy — making the entire .find() call unreachable/pointless. The same pattern exists in deleteEvent.

Evidence:

  • Line 253: const selectedCalendar = externalCalendarId ? externalCalendarId : event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId;
  • When externalCalendarId is falsy, the .find() predicate cal.externalId === externalCalendarId compares against a falsy value, so it can never match a real externalId. The result will always be undefined.
  • This means the fallback effectively never provides a useful calendar ID, silently falling through to undefined.
  • Same issue at line ~312 in deleteEvent.

Agent: performance

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [Grapple PR] Suggested fix — performance agent (Small fix (4 lines, 1 file))

Dead-code logic in updateEvent: the selectedCalendar fallback uses Array.find to search by externalCalendarId but the condition cal.externalId === externalCalendarId will always be true for the matched element and is used only when externalCalendarId is falsy — making the entire .find() call unreachable/pointless. The same pattern exists in deleteEvent.

--- a/packages/app-store/googlecalendar/lib/CalendarService.ts
+++ b/packages/app-store/googlecalendar/lib/CalendarService.ts
@@ -253,7 +253,7 @@ export default class GoogleCalendarService implements Calendar {
 
       const selectedCalendar = externalCalendarId
         ? externalCalendarId
-        : event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId;
+        : mainHostDestinationCalendar?.externalId;
 
       calendar.events.update(
         {
@@ -312,7 +312,7 @@ export default class GoogleCalendarService implements Calendar {
       const defaultCalendarId = "primary";
       const calendarId = externalCalendarId
         ? externalCalendarId
-        : event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId;
+        : mainHostDestinationCalendar?.externalId;
 
       calendar.events.delete(
         {

🤖 Grapple PR auto-fix • major • Review this diff before applying

calendar.events.update(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 MAJOR — Input validation (confidence: 100%)

In updateEvent, the selectedCalendar logic contains a tautological find: event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId. When externalCalendarId is truthy (the first branch), the found value will always equal externalCalendarId itself, making the array lookup pointless. When externalCalendarId is falsy (second branch), the find returns undefined because nothing matches undefined. The effective result is always externalCalendarId || undefined, which is the same as the pre-refactor code but loses the credentialId-scoped lookup, potentially allowing updates to wrong calendars.

Evidence:

  • Line 253: const selectedCalendar = externalCalendarId ? externalCalendarId : event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId
  • When externalCalendarId is falsy, searching for cal.externalId === externalCalendarId is searching for cal.externalId === undefined/null/'' which won't match real calendar IDs
  • Same pattern at line 312 in deleteEvent
  • This means for updates without an explicit externalCalendarId, no calendar is selected and Google API receives undefined as calendarId

Agent: security

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [Grapple PR] Suggested fix — security agent (Small fix (4 lines, 1 file))

In updateEvent, the selectedCalendar logic contains a tautological find: event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId. When externalCalendarId is truthy (the first branch), the found value will always equal externalCalendarId itself, making the array lookup pointless. When externalCalendarId is falsy (second branch), the find returns undefined because nothing matches undefined. The effective result is always externalCalendarId || undefined, which is the same as the pre-refactor code but loses the credentialId-scoped lookup, potentially allowing updates to wrong calendars.

--- a/packages/app-store/googlecalendar/lib/CalendarService.ts
+++ b/packages/app-store/googlecalendar/lib/CalendarService.ts
@@ -253,7 +253,7 @@ export default class GoogleCalendarService implements Calendar {
 
       const selectedCalendar = externalCalendarId
         ? externalCalendarId
-        : event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId;
+        : event.destinationCalendar?.find((cal) => cal.credentialId === this.credential.id)?.externalId;
 
       calendar.events.update(
         {
@@ -312,7 +312,7 @@ export default class GoogleCalendarService implements Calendar {
       const defaultCalendarId = "primary";
       const calendarId = externalCalendarId
         ? externalCalendarId
-        : event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId;
+        : event.destinationCalendar?.find((cal) => cal.credentialId === this.credential.id)?.externalId;
 
       calendar.events.delete(
         {

🤖 Grapple PR auto-fix • major • Review this diff before applying

{
Expand Down Expand Up @@ -303,7 +312,9 @@ export default class GoogleCalendarService implements Calendar {
});

const defaultCalendarId = "primary";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 CRITICAL — Logic Bug - Dead Code / Always Falsy (confidence: 100%)

In deleteEvent, the exact same logic bug as updateEvent: the fallback branch searches event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId but this branch only executes when externalCalendarId is falsy, so find() will search for cal.externalId === undefined and return nothing. Calendar event deletions will fail to find the correct calendar ID.

Evidence:

  • Line 312-314: const calendarId = externalCalendarId ? externalCalendarId : event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId;
  • Same pattern as updateEvent — the find() in the else branch compares against a falsy externalCalendarId
  • Original code: event.destinationCalendar?.externalId

Agent: logic

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [Grapple PR] Suggested fix — logic agent (Small fix (8 lines, 1 file))

In deleteEvent, the exact same logic bug as updateEvent: the fallback branch searches event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId but this branch only executes when externalCalendarId is falsy, so find() will search for cal.externalId === undefined and return nothing. Calendar event deletions will fail to find the correct calendar ID.

--- a/packages/app-store/googlecalendar/lib/CalendarService.ts
+++ b/packages/app-store/googlecalendar/lib/CalendarService.ts
@@ -250,7 +250,7 @@ export default class GoogleCalendarService implements Calendar {
 
       const selectedCalendar = externalCalendarId
         ? externalCalendarId
-        : event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId;
+        : mainHostDestinationCalendar?.externalId;
 
       calendar.events.update(
         {
@@ -309,6 +309,9 @@ export default class GoogleCalendarService implements Calendar {
   async deleteEvent(uid: string, event: CalendarEvent, externalCalendarId?: string | null): Promise<void> {
     return new Promise(async (resolve, reject) => {
       const myGoogleAuth = await this.auth.getToken();
+      const [mainHostDestinationCalendar] =
+        event?.destinationCalendar && event?.destinationCalendar.length > 0
+          ? event.destinationCalendar
+          : [];
       const calendar = google.calendar({
         version: "v3",
         auth: myGoogleAuth,
@@ -316,7 +319,7 @@ export default class GoogleCalendarService implements Calendar {
       const defaultCalendarId = "primary";
       const calendarId = externalCalendarId
         ? externalCalendarId
-        : event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId;
+        : mainHostDestinationCalendar?.externalId;
 
       calendar.events.delete(
         {

🤖 Grapple PR auto-fix • critical • Review this diff before applying

const calendarId = externalCalendarId ? externalCalendarId : event.destinationCalendar?.externalId;
const calendarId = externalCalendarId

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MINOR — Unnecessary work (confidence: 93%)

Same no-op fallback pattern in deleteEvent: event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId when externalCalendarId is falsy will never find a matching entry.

Evidence:

  • const calendarId = externalCalendarId ? externalCalendarId : event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId;
  • The condition in .find uses the same falsy externalCalendarId as the comparison value.
  • This means the fallback always returns undefined, leaving calendarId undefined instead of falling back to the host's destination calendar.

Agent: performance

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [Grapple PR] Suggested fix — performance agent (Small fix (4 lines, 1 file))

Same no-op fallback pattern in deleteEvent: event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId when externalCalendarId is falsy will never find a matching entry.

--- a/packages/app-store/googlecalendar/lib/CalendarService.ts
+++ b/packages/app-store/googlecalendar/lib/CalendarService.ts
@@ -250,7 +250,7 @@ export default class GoogleCalendarService implements Calendar {
 
       const selectedCalendar = externalCalendarId
         ? externalCalendarId
-        : event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId;
+        : event.destinationCalendar?.[0]?.externalId;
 
       calendar.events.update(
         {
@@ -312,7 +312,7 @@ export default class GoogleCalendarService implements Calendar {
 
       const defaultCalendarId = "primary";
       const calendarId = externalCalendarId
         ? externalCalendarId
-        : event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId;
+        : event.destinationCalendar?.[0]?.externalId;
 
       calendar.events.delete(
         {

🤖 Grapple PR auto-fix • minor • Review this diff before applying

? externalCalendarId
: event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId;

calendar.events.delete(
{
Expand Down
12 changes: 8 additions & 4 deletions packages/app-store/larkcalendar/lib/CalendarService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ export default class LarkCalendarService implements Calendar {
async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
let eventId = "";
let eventRespData;
const calendarId = event.destinationCalendar?.externalId;
const [mainHostDestinationCalendar] = event.destinationCalendar ?? [];
const calendarId = mainHostDestinationCalendar?.externalId;
if (!calendarId) {
throw new Error("no calendar id");
}
Expand Down Expand Up @@ -160,7 +161,8 @@ export default class LarkCalendarService implements Calendar {
}

private createAttendees = async (event: CalendarEvent, eventId: string) => {
const calendarId = event.destinationCalendar?.externalId;
const [mainHostDestinationCalendar] = event.destinationCalendar ?? [];
const calendarId = mainHostDestinationCalendar?.externalId;
if (!calendarId) {
this.log.error("no calendar id provided in createAttendees");
throw new Error("no calendar id provided in createAttendees");
Expand All @@ -187,7 +189,8 @@ export default class LarkCalendarService implements Calendar {
async updateEvent(uid: string, event: CalendarEvent, externalCalendarId?: string) {
const eventId = uid;
let eventRespData;
const calendarId = externalCalendarId || event.destinationCalendar?.externalId;
const [mainHostDestinationCalendar] = event.destinationCalendar ?? [];
const calendarId = externalCalendarId || mainHostDestinationCalendar?.externalId;
if (!calendarId) {
this.log.error("no calendar id provided in updateEvent");
throw new Error("no calendar id provided in updateEvent");
Expand Down Expand Up @@ -231,7 +234,8 @@ export default class LarkCalendarService implements Calendar {
* @returns
*/
async deleteEvent(uid: string, event: CalendarEvent, externalCalendarId?: string) {
const calendarId = externalCalendarId || event.destinationCalendar?.externalId;
const [mainHostDestinationCalendar] = event.destinationCalendar ?? [];
const calendarId = externalCalendarId || mainHostDestinationCalendar?.externalId;
if (!calendarId) {
this.log.error("no calendar id provided in deleteEvent");
throw new Error("no calendar id provided in deleteEvent");
Expand Down
5 changes: 3 additions & 2 deletions packages/app-store/office365calendar/lib/CalendarService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,10 @@ export default class Office365CalendarService implements Calendar {
}

async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
const [mainHostDestinationCalendar] = event.destinationCalendar ?? [];
try {
const eventsUrl = event.destinationCalendar?.externalId
? `/me/calendars/${event.destinationCalendar?.externalId}/events`
const eventsUrl = mainHostDestinationCalendar?.externalId
? `/me/calendars/${mainHostDestinationCalendar?.externalId}/events`
: "/me/calendar/events";

const response = await this.fetcher(eventsUrl, {
Expand Down
43 changes: 24 additions & 19 deletions packages/core/CalendarManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,8 @@ export const getBusyCalendarTimes = async (

export const createEvent = async (
credential: CredentialWithAppName,
calEvent: CalendarEvent
calEvent: CalendarEvent,
externalId?: string
): Promise<EventResult<NewCalendarEventType>> => {
const uid: string = getUid(calEvent);
const calendar = await getCalendar(credential);
Expand All @@ -226,29 +227,31 @@ export const createEvent = async (

// Check if the disabledNotes flag is set to true
if (calEvent.hideCalendarNotes) {
calEvent.additionalNotes = "Notes have been hidden by the organiser"; // TODO: i18n this string?
calEvent.additionalNotes = "Notes have been hidden by the organizer"; // TODO: i18n this string?
}

// TODO: Surface success/error messages coming from apps to improve end user visibility
const creationResult = calendar
? await calendar.createEvent(calEvent).catch(async (error: { code: number; calError: string }) => {
success = false;
/**
* There is a time when selectedCalendar externalId doesn't match witch certain credential
* so google returns 404.
* */
if (error?.code === 404) {
? await calendar
.createEvent(calEvent, credential.id)
.catch(async (error: { code: number; calError: string }) => {
success = false;
/**
* There is a time when selectedCalendar externalId doesn't match witch certain credential
* so google returns 404.
* */
if (error?.code === 404) {
return undefined;
}
if (error?.calError) {
calError = error.calError;
}
log.error("createEvent failed", JSON.stringify(error), calEvent);
// @TODO: This code will be off till we can investigate an error with it
//https://github.com/calcom/cal.com/issues/3949
// await sendBrokenIntegrationEmail(calEvent, "calendar");
return undefined;
}
if (error?.calError) {
calError = error.calError;
}
log.error("createEvent failed", JSON.stringify(error), calEvent);
// @TODO: This code will be off till we can investigate an error with it
//https://github.com/calcom/cal.com/issues/3949
// await sendBrokenIntegrationEmail(calEvent, "calendar");
return undefined;
})
})
: undefined;

return {
Expand All @@ -261,6 +264,8 @@ export const createEvent = async (
originalEvent: calEvent,
calError,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 MAJOR — Incorrect externalId Passing (confidence: 100%)

The createEvent function now returns externalId from the third parameter passed to it. However, in the fallback path where destination.credentialId is not set (line 381-385 in EventManager.ts), createEvent is called without the externalId parameter: await createEvent(c, event). This means externalId will be undefined in the result, and the booking reference's externalCalendarId won't be set for these events.

Evidence:

  • CalendarManager.ts line 219: externalId?: string parameter added
  • CalendarManager.ts line 265: externalId returned in result
  • EventManager.ts line 384: await createEvent(c, event) — no externalId passed
  • EventManager.ts line 374: await createEvent(credential, event, destination.externalId) — correctly passes externalId

Agent: logic

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Grapple PR] Auto-fix — logic agent (Small fix (2 lines, 1 file))

The createEvent function now returns externalId from the third parameter passed to it. However, in the fallback path where destination.credentialId is not set (line 381-385 in EventManager.ts), createEvent is called without the externalId parameter: await createEvent(c, event). This means externalId will be undefined in the result, and the booking reference's externalCalendarId won't be set for these events.

Suggested change
calError,
results.push(await createEvent(c, event, c.externalId));

🤖 Grapple PR auto-fix • major • confidence: 100%

calWarnings: creationResult?.additionalInfo?.calWarnings || [],
externalId,
credentialId: credential.id,
};
};

Expand Down
Loading