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 packages/app-store/_utils/getCalendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import appStore from "..";

const log = logger.getChildLogger({ prefix: ["CalendarManager"] });

export const getCalendar = (credential: CredentialPayload | null): Calendar | null => {
export const getCalendar = async (credential: CredentialPayload | null): Promise<Calendar | null> => {
if (!credential || !credential.key) return null;
let { type: calendarType } = credential;
if (calendarType?.endsWith("_other_calendar")) {
calendarType = calendarType.split("_other_calendar")[0];
}
const calendarApp = appStore[calendarType.split("_").join("") as keyof typeof appStore];
const calendarApp = await appStore[calendarType.split("_").join("") as keyof typeof appStore];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 AI 代码审查发现问题

📋 问题概述

在动态属性访问中,未对 calendarType 的值进行充分校验。如果 calendarType 包含恶意构造的字符串(如特殊字符或非法路径),可能导致模块加载失败、任意模块导入或执行,存在代码注入和路径遍历风险。split("_").join("") 操作也可能因输入异常导致拼接错误,引发运行时错误。

📍 问题详情

🔴 问题 1 | 严重程度: HIGH | 行号: 15

💬 详细说明:

  • 攻击者可利用此漏洞访问或执行 appStore 中的任意模块,可能导致服务器端请求伪造、远程代码执行或其他未授权操作。

📝 问题代码:

const calendarApp = await appStore[calendarType.split("_").join("") as keyof typeof appStore];

💡 修复建议:

在使用 calendarType 构造键名前,应验证其是否为预定义的有效类型之一。建议维护一个白名单列表,仅允许已知合法的 calendar 类型通过。同时,确保 appStore 中的键名与实际模块路径严格对应,避免任意字符串拼接导致的不安全访问。

✅ 修复示例:

const validCalendarTypes = new Set(['applecalendar', 'caldavcalendar', 'closecom', 'dailyvideo', 'googlecalendar', 'googlevideo', 'hubspot', 'huddle01video', 'jitsivideo', 'larkcalendar', 'office365calendar', 'office365video', 'plausible', 'salesforce', 'zohocrm', 'sendgrid', 'stripepayment', 'tandemvideo', 'vital', 'zoomvideo', 'wipemycalother', 'giphy', 'zapier', 'exchange2013calendar', 'exchange2016calendar', 'exchangecalendar', 'facetime', 'sylapsvideo']);
const normalizedType = calendarType.split('_').join('');
if (!validCalendarTypes.has(normalizedType)) {
  log.warn(`Invalid calendar type: ${calendarType}`);
  return null;
}
const calendarApp = await appStore[normalizedType as keyof typeof appStore];

🔗 参考链接

if (!(calendarApp && "lib" in calendarApp && "CalendarService" in calendarApp.lib)) {
log.warn(`calendar of type ${calendarType} is not implemented`);
return null;
Expand Down
88 changes: 29 additions & 59 deletions packages/app-store/index.ts
Original file line number Diff line number Diff line change
@@ -1,63 +1,33 @@
// import * as example from "./_example";
import * as applecalendar from "./applecalendar";
import * as caldavcalendar from "./caldavcalendar";
import * as closecom from "./closecom";
import * as dailyvideo from "./dailyvideo";
import * as exchange2013calendar from "./exchange2013calendar";
import * as exchange2016calendar from "./exchange2016calendar";
import * as exchangecalendar from "./exchangecalendar";
import * as facetime from "./facetime";
import * as giphy from "./giphy";
import * as googlecalendar from "./googlecalendar";
import * as googlevideo from "./googlevideo";
import * as hubspot from "./hubspot";
import * as huddle01video from "./huddle01video";
import * as jitsivideo from "./jitsivideo";
import * as larkcalendar from "./larkcalendar";
import * as office365calendar from "./office365calendar";
import * as office365video from "./office365video";
import * as plausible from "./plausible";
import * as salesforce from "./salesforce";
import * as sendgrid from "./sendgrid";
import * as stripepayment from "./stripepayment";
import * as sylapsvideo from "./sylapsvideo";
import * as tandemvideo from "./tandemvideo";
import * as vital from "./vital";
import * as wipemycalother from "./wipemycalother";
import * as zapier from "./zapier";
import * as zohocrm from "./zohocrm";
import * as zoomvideo from "./zoomvideo";

const appStore = {
// example,
applecalendar,
caldavcalendar,
closecom,
dailyvideo,
googlecalendar,
googlevideo,
hubspot,
huddle01video,
jitsivideo,
sylapsvideo,
larkcalendar,
office365calendar,
office365video,
plausible,
salesforce,
zohocrm,
sendgrid,
stripepayment,
tandemvideo,
vital,
zoomvideo,
wipemycalother,
giphy,
zapier,
exchange2013calendar,
exchange2016calendar,
exchangecalendar,
facetime,
// example: import("./example"),
applecalendar: import("./applecalendar"),
caldavcalendar: import("./caldavcalendar"),
closecom: import("./closecom"),
dailyvideo: import("./dailyvideo"),
googlecalendar: import("./googlecalendar"),
googlevideo: import("./googlevideo"),
hubspot: import("./hubspot"),
huddle01video: import("./huddle01video"),
jitsivideo: import("./jitsivideo"),
larkcalendar: import("./larkcalendar"),
office365calendar: import("./office365calendar"),
office365video: import("./office365video"),
plausible: import("./plausible"),
salesforce: import("./salesforce"),
zohocrm: import("./zohocrm"),
sendgrid: import("./sendgrid"),
stripepayment: import("./stripepayment"),
tandemvideo: import("./tandemvideo"),
vital: import("./vital"),
zoomvideo: import("./zoomvideo"),
wipemycalother: import("./wipemycalother"),
giphy: import("./giphy"),
zapier: import("./zapier"),
exchange2013calendar: import("./exchange2013calendar"),
exchange2016calendar: import("./exchange2016calendar"),
exchangecalendar: import("./exchangecalendar"),
facetime: import("./facetime"),
sylapsvideo: import("./sylapsvideo"),
};

export default appStore;
4 changes: 2 additions & 2 deletions packages/app-store/vital/lib/reschedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,10 @@ const Reschedule = async (bookingUid: string, cancellationReason: string) => {
(ref) => !!credentialsMap.get(ref.type)
);
try {
bookingRefsFiltered.forEach((bookingRef) => {
bookingRefsFiltered.forEach(async (bookingRef) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 AI 代码审查发现问题

📋 问题概述

使用 forEach 遍历数组并执行异步操作,但 forEach 不会等待异步任务完成,导致所有异步操作被丢弃,无法捕获异常,造成资源泄漏或逻辑失败。

📍 问题详情

🟡 问题 1 | 严重程度: MEDIUM | 行号: 125

💬 详细说明:

  • 使用 forEach 遍历数组并执行异步操作,但 forEach 不会等待异步任务完成,导致所有异步操作被丢弃,无法捕获异常,造成资源泄漏或逻辑失败。

📝 问题代码:

bookingRefsFiltered.forEach(async (bookingRef) => {

💡 修复建议:

将 forEach 替换为 for...of 循环,并在每次迭代中使用 await 等待异步操作完成,确保每个任务都被正确执行和错误处理。

✅ 修复示例:

for (const bookingRef of bookingRefsFiltered) {
  if (bookingRef.uid) {
    if (bookingRef.type.endsWith('_calendar')) {
      const calendar = await getCalendar(credentialsMap.get(bookingRef.type));
      if (calendar) {
        await calendar.deleteEvent(bookingRef.uid, builder.calendarEvent);
      }
    } else if (bookingRef.type.endsWith('_video')) {
      await deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid);
    }
  }
}

🔗 参考链接

if (bookingRef.uid) {
if (bookingRef.type.endsWith("_calendar")) {
const calendar = getCalendar(credentialsMap.get(bookingRef.type));
const calendar = await getCalendar(credentialsMap.get(bookingRef.type));
return calendar?.deleteEvent(bookingRef.uid, builder.calendarEvent);
} else if (bookingRef.type.endsWith("_video")) {
return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid);
Expand Down
4 changes: 2 additions & 2 deletions packages/app-store/wipemycalother/lib/reschedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,10 @@ const Reschedule = async (bookingUid: string, cancellationReason: string) => {
(ref) => !!credentialsMap.get(ref.type)
);
try {
bookingRefsFiltered.forEach((bookingRef) => {
bookingRefsFiltered.forEach(async (bookingRef) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 AI 代码审查发现问题

📋 问题概述

在 forEach 中使用 async 回调但未等待其完成,导致异步操作可能未执行完毕就继续执行后续逻辑。这可能导致日历或视频会议取消失败,且错误处理不完整(catch 块仅捕获外层异常,无法捕获内部 async 错误)。此外,若多个并发请求失败,仅记录第一个错误,其余被忽略。

📍 问题详情

🔴 问题 1 | 严重程度: HIGH | 行号: 125

💬 详细说明:

  • 日历事件可能不会被正确删除,导致数据不一致;异步操作未完成可能导致资源泄漏和竞态条件,存在安全风险。

📝 问题代码:

bookingRefsFiltered.forEach(async (bookingRef) => {

💡 修复建议:

将 forEach 替换为 for...of 遍历,并使用 await 等待每个异步操作完成;或使用 Promise.allSettled 处理所有异步任务并统一处理结果,确保所有取消操作都被正确执行和监控。

✅ 修复示例:

const promises = bookingRefsFiltered.map(async (bookingRef) => {
  if (bookingRef.uid) {
    if (bookingRef.type.endsWith("_calendar")) {
      const calendar = await getCalendar(credentialsMap.get(bookingRef.type));
      return calendar?.deleteEvent(bookingRef.uid, builder.calendarEvent);
    } else if (bookingRef.type.endsWith("_video")) {
      return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid);
    }
  }
});
await Promise.allSettled(promises);

🔗 参考链接

if (bookingRef.uid) {
if (bookingRef.type.endsWith("_calendar")) {
const calendar = getCalendar(credentialsMap.get(bookingRef.type));
const calendar = await getCalendar(credentialsMap.get(bookingRef.type));
return calendar?.deleteEvent(bookingRef.uid, builder.calendarEvent);
} else if (bookingRef.type.endsWith("_video")) {
return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid);
Expand Down
15 changes: 8 additions & 7 deletions packages/core/CalendarManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const getCalendarCredentials = (credentials: Array<CredentialPayload>) =>
const calendar = getCalendar(credential);
return app.variant === "calendar" ? [{ integration: app, credential, calendar }] : [];
});

return credentials.length ? credentials : [];
});

Expand All @@ -43,8 +44,8 @@ export const getConnectedCalendars = async (
const connectedCalendars = await Promise.all(
calendarCredentials.map(async (item) => {
try {
const { calendar, integration, credential } = item;

const { integration, credential } = item;
const calendar = await item.calendar;
// Don't leak credentials to the client
const credentialId = credential.id;
if (!calendar) {
Expand Down Expand Up @@ -138,7 +139,7 @@ export const getCachedResults = async (
selectedCalendars: SelectedCalendar[]
): Promise<EventBusyDate[][]> => {
const calendarCredentials = withCredentials.filter((credential) => credential.type.endsWith("_calendar"));
const calendars = calendarCredentials.map((credential) => getCalendar(credential));
const calendars = await Promise.all(calendarCredentials.map((credential) => getCalendar(credential)));
performance.mark("getBusyCalendarTimesStart");
const results = calendars.map(async (c, i) => {
/** Filter out nulls */
Expand Down Expand Up @@ -229,7 +230,7 @@ export const createEvent = async (
calEvent: CalendarEvent
): Promise<EventResult<NewCalendarEventType>> => {
const uid: string = getUid(calEvent);
const calendar = getCalendar(credential);
const calendar = await getCalendar(credential);
let success = true;
let calError: string | undefined = undefined;

Expand Down Expand Up @@ -280,7 +281,7 @@ export const updateEvent = async (
externalCalendarId: string | null
): Promise<EventResult<NewCalendarEventType>> => {
const uid = getUid(calEvent);
const calendar = getCalendar(credential);
const calendar = await getCalendar(credential);
let success = false;
let calError: string | undefined = undefined;
let calWarnings: string[] | undefined = [];
Expand Down Expand Up @@ -326,12 +327,12 @@ export const updateEvent = async (
};
};

export const deleteEvent = (
export const deleteEvent = async (
credential: CredentialPayload,
uid: string,
event: CalendarEvent
): Promise<unknown> => {
const calendar = getCalendar(credential);
const calendar = await getCalendar(credential);
if (calendar) {
return calendar.deleteEvent(uid, event);
}
Expand Down
3 changes: 1 addition & 2 deletions packages/core/EventManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,8 +485,7 @@ export default class EventManager {
id: oldCalendarEvent.credentialId,
},
});
const calendar = getCalendar(calendarCredential);

const calendar = await getCalendar(calendarCredential);
await calendar?.deleteEvent(oldCalendarEvent.uid, event, oldCalendarEvent.externalCalendarId);
}
}
Expand Down
36 changes: 20 additions & 16 deletions packages/core/videoClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,25 @@ const log = logger.getChildLogger({ prefix: ["[lib] videoClient"] });
const translator = short();

// factory
const getVideoAdapters = (withCredentials: CredentialPayload[]): VideoApiAdapter[] =>
withCredentials.reduce<VideoApiAdapter[]>((acc, cred) => {
const getVideoAdapters = async (withCredentials: CredentialPayload[]): Promise<VideoApiAdapter[]> => {
const videoAdapters: VideoApiAdapter[] = [];

for (const cred of withCredentials) {
const appName = cred.type.split("_").join(""); // Transform `zoom_video` to `zoomvideo`;
const app = appStore[appName as keyof typeof appStore];
const app = await appStore[appName as keyof typeof appStore];

if (app && "lib" in app && "VideoApiAdapter" in app.lib) {
const makeVideoApiAdapter = app.lib.VideoApiAdapter as VideoApiAdapterFactory;
const videoAdapter = makeVideoApiAdapter(cred);
acc.push(videoAdapter);
return acc;
videoAdapters.push(videoAdapter);
}
return acc;
}, []);
}

return videoAdapters;
};

const getBusyVideoTimes = (withCredentials: CredentialPayload[]) =>
Promise.all(getVideoAdapters(withCredentials).map((c) => c?.getAvailability())).then((results) =>
const getBusyVideoTimes = async (withCredentials: CredentialPayload[]) =>
Promise.all((await getVideoAdapters(withCredentials)).map((c) => c?.getAvailability())).then((results) =>
results.reduce((acc, availability) => acc.concat(availability), [] as (EventBusyDate | undefined)[])
);

Expand All @@ -45,7 +49,7 @@ const createMeeting = async (credential: CredentialWithAppName, calEvent: Calend
);
}

const videoAdapters = getVideoAdapters([credential]);
const videoAdapters = await getVideoAdapters([credential]);
const [firstVideoAdapter] = videoAdapters;
let createdMeeting;
let returnObject: {
Expand Down Expand Up @@ -104,7 +108,7 @@ const updateMeeting = async (

let success = true;

const [firstVideoAdapter] = getVideoAdapters([credential]);
const [firstVideoAdapter] = await getVideoAdapters([credential]);
const updatedMeeting =
credential && bookingRef
? await firstVideoAdapter?.updateMeeting(bookingRef, calEvent).catch(async (e) => {
Expand Down Expand Up @@ -135,9 +139,9 @@ const updateMeeting = async (
};
};

const deleteMeeting = (credential: CredentialPayload, uid: string): Promise<unknown> => {
const deleteMeeting = async (credential: CredentialPayload, uid: string): Promise<unknown> => {
if (credential) {
const videoAdapter = getVideoAdapters([credential])[0];
const videoAdapter = (await getVideoAdapters([credential]))[0];
// There are certain video apps with no video adapter defined. e.g. riverby,whereby
if (videoAdapter) {
return videoAdapter.deleteMeeting(uid);
Expand All @@ -155,7 +159,7 @@ const createMeetingWithCalVideo = async (calEvent: CalendarEvent) => {
} catch (e) {
return;
}
const [videoAdapter] = getVideoAdapters([
const [videoAdapter] = await getVideoAdapters([
{
id: 0,
appId: "daily-video",
Expand All @@ -178,7 +182,7 @@ const getRecordingsOfCalVideoByRoomName = async (
console.error("Error: Cal video provider is not installed.");
return;
}
const [videoAdapter] = getVideoAdapters([
const [videoAdapter] = await getVideoAdapters([
{
id: 0,
appId: "daily-video",
Expand All @@ -199,7 +203,7 @@ const getDownloadLinkOfCalVideoByRecordingId = async (recordingId: string) => {
console.error("Error: Cal video provider is not installed.");
return;
}
const [videoAdapter] = getVideoAdapters([
const [videoAdapter] = await getVideoAdapters([
{
id: 0,
appId: "daily-video",
Expand Down
23 changes: 12 additions & 11 deletions packages/features/bookings/lib/handleCancelBooking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ async function handler(req: CustomRequest) {
integrationsToDelete.push(deleteMeeting(credential, reference.uid));
}
if (reference.type.includes("_calendar")) {
const calendar = getCalendar(credential);
const calendar = await getCalendar(credential);
if (calendar) {
integrationsToDelete.push(
calendar?.deleteEvent(reference.uid, evt, reference.externalCalendarId)
Expand All @@ -262,7 +262,7 @@ async function handler(req: CustomRequest) {
);
}
if (reference.type.includes("_calendar")) {
const calendar = getCalendar(credential);
const calendar = await getCalendar(credential);
if (calendar) {
integrationsToDelete.push(
calendar?.updateEvent(reference.uid, updatedEvt, reference.externalCalendarId)
Expand Down Expand Up @@ -449,7 +449,7 @@ async function handler(req: CustomRequest) {
(credential) => credential.id === credentialId
);
if (calendarCredential) {
const calendar = getCalendar(calendarCredential);
const calendar = await getCalendar(calendarCredential);
if (
bookingToDelete.eventType?.recurringEvent &&
bookingToDelete.recurringEventId &&
Expand All @@ -458,7 +458,7 @@ async function handler(req: CustomRequest) {
bookingToDelete.user.credentials
.filter((credential) => credential.type.endsWith("_calendar"))
.forEach(async (credential) => {
const calendar = getCalendar(credential);
const calendar = await getCalendar(credential);
for (const updBooking of updatedBookings) {
const bookingRef = updBooking.references.find((ref) => ref.type.includes("_calendar"));
if (bookingRef) {
Expand All @@ -474,12 +474,13 @@ async function handler(req: CustomRequest) {
}
} else {
// For bookings made before the refactor we go through the old behaviour of running through each calendar credential
bookingToDelete.user.credentials
.filter((credential) => credential.type.endsWith("_calendar"))
.forEach((credential) => {
const calendar = getCalendar(credential);
apiDeletes.push(calendar?.deleteEvent(uid, evt, externalCalendarId) as Promise<unknown>);
});
const calendarCredentials = bookingToDelete.user.credentials.filter((credential) =>
credential.type.endsWith("_calendar")
);
for (const credential of calendarCredentials) {
const calendar = await getCalendar(credential);
apiDeletes.push(calendar?.deleteEvent(uid, evt, externalCalendarId) as Promise<unknown>);
}
}
}

Expand Down Expand Up @@ -585,7 +586,7 @@ async function handler(req: CustomRequest) {
}

// Posible to refactor TODO:
const paymentApp = appStore[paymentAppCredential?.app?.dirName as keyof typeof appStore];
const paymentApp = await appStore[paymentAppCredential?.app?.dirName as keyof typeof appStore];
if (!(paymentApp && "lib" in paymentApp && "PaymentService" in paymentApp.lib)) {
console.warn(`payment App service of type ${paymentApp} is not implemented`);
return null;
Expand Down
2 changes: 1 addition & 1 deletion packages/features/bookings/lib/handleNewBooking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -885,7 +885,7 @@ async function handler(
integrationsToDelete.push(deleteMeeting(credential, reference.uid));
}
if (reference.type.includes("_calendar") && originalBookingEvt) {
const calendar = getCalendar(credential);
const calendar = await getCalendar(credential);
if (calendar) {
integrationsToDelete.push(
calendar?.deleteEvent(reference.uid, originalBookingEvt, reference.externalCalendarId)
Expand Down
2 changes: 1 addition & 1 deletion packages/lib/payment/deletePayment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const deletePayment = async (
} | null;
}
): Promise<boolean> => {
const paymentApp = appStore[paymentAppCredentials?.app?.dirName as keyof typeof appStore];
const paymentApp = await appStore[paymentAppCredentials?.app?.dirName as keyof typeof appStore];
if (!(paymentApp && "lib" in paymentApp && "PaymentService" in paymentApp.lib)) {
console.warn(`payment App service of type ${paymentApp} is not implemented`);
return false;
Expand Down
2 changes: 1 addition & 1 deletion packages/lib/payment/handlePayment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const handlePayment = async (
uid: string;
}
) => {
const paymentApp = appStore[paymentAppCredentials?.app?.dirName as keyof typeof appStore];
const paymentApp = await appStore[paymentAppCredentials?.app?.dirName as keyof typeof appStore];
if (!(paymentApp && "lib" in paymentApp && "PaymentService" in paymentApp.lib)) {
console.warn(`payment App service of type ${paymentApp} is not implemented`);
return null;
Expand Down
Loading