Skip to content

Comments

perf: improve /apps/installed/calendar page load speed.#23198

Closed
zhyd1997 wants to merge 30 commits intocalcom:mainfrom
zhyd1997:perf/issue-23104-1755628640
Closed

perf: improve /apps/installed/calendar page load speed.#23198
zhyd1997 wants to merge 30 commits intocalcom:mainfrom
zhyd1997:perf/issue-23104-1755628640

Conversation

@zhyd1997
Copy link
Contributor

@zhyd1997 zhyd1997 commented Aug 19, 2025

What does this PR do?

Before

○ Compiling /apps/installed/[category] ...
✓ Compiled /apps/installed/[category] in 13.6s

After

 ○ Compiling /apps/installed/[category] ...
 ✓ Compiled /apps/installed/[category] in 5.4s

Related to #23104

  • Fixes #XXXX (GitHub issue number)
  • Fixes CAL-XXXX (Linear issue number - should be visible at the bottom of the GitHub issue description)

Visual Demo (For contributors especially)

A visual demonstration is strongly recommended, for both the original and new change (video / image - any one).

Video Demo (if applicable):

  • Show screen recordings of the issue or feature.
  • Demonstrate how to reproduce the issue, the behavior before and after the change.

Image Demo (if applicable):

  • Add side-by-side screenshots of the original and updated change.
  • Highlight any significant change(s).

Mandatory Tasks (DO NOT REMOVE)

  • I have self-reviewed the code (A decent size PR without self-review might be rejected).
  • I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. If N/A, write N/A here and check the checkbox.
  • I confirm automated tests are in place that prove my fix is effective or that my feature works.

How should this be tested?

  • Are there environment variables that should be set?
  • What are the minimal test data to have?
  • What is expected (happy path) to have (input and output)?
  • Any other important info that could help to test that PR

Checklist

  • I haven't read the contributing guide
  • My code doesn't follow the style guidelines of this project
  • I haven't commented my code, particularly in hard-to-understand areas
  • I haven't checked if my changes generate no new warnings

@vercel
Copy link

vercel bot commented Aug 19, 2025

@zhyd1997 is attempting to deploy a commit to the cal Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 19, 2025

Walkthrough

Adds a cached dynamic-import helper and per-domain loader registries (calendarLoaders, videoLoaders, paymentLoaders, analyticsLoaders). Generates and normalizes calendar and video metadata (calendarApps.metadata.generated.ts, videoApps.metadata.generated.ts, calendarAppsMetaData, videoAppsMetaData) and introduces prepareAppsWithCredentials and sanitizeAppsMetadata. Exposes getCalendarApps and getVideoApps helpers and wires callers (EventManager, CalendarManager, videoClient, TRPC payment handlers, etc.) to use the new loaders and helpers. Adds test mocks for the new loaders and updates test helpers accordingly.

Possibly related PRs

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@zhyd1997 zhyd1997 mentioned this pull request Aug 20, 2025
@zhyd1997 zhyd1997 marked this pull request as ready for review August 20, 2025 14:07
@graphite-app graphite-app bot added the community Created by Linear-GitHub Sync label Aug 20, 2025
@graphite-app graphite-app bot requested a review from a team August 20, 2025 14:07
@dosubot dosubot bot added the performance area: performance, page load, slow, slow endpoints, loading screen, unresponsive label Aug 20, 2025
@dosubot
Copy link

dosubot bot commented Aug 20, 2025

Related Documentation

No published documentation to review for changes on this repository.
Write your first living document

How did I do? Any feedback?  Join Discord

@graphite-app
Copy link

graphite-app bot commented Aug 20, 2025

Graphite Automations

"Add consumer team as reviewer" took an action on this PR • (08/20/25)

1 reviewer was added to this PR based on Keith Williams's automation.

"Add community label" took an action on this PR • (08/20/25)

1 label was added to this PR based on Keith Williams's automation.

"Add foundation team as reviewer" took an action on this PR • (08/21/25)

1 reviewer was added to this PR based on Keith Williams's automation.

@kart1ka
Copy link
Contributor

kart1ka commented Aug 20, 2025

cc @keithwillcode for review

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/trpc/server/routers/viewer/payments.tsx (1)

124-161: Don’t leak raw errors to clients

Returning interpolated errors can expose internals/PII. Log server-side and return a generic message.

Apply this diff:

       } catch (err) {
-        throw new TRPCError({
-          code: "INTERNAL_SERVER_ERROR",
-          message: `Error processing payment with error ${err}`,
-        });
+        // Consider logging the full error server-side (e.g., with logger.error)
+        throw new TRPCError({
+          code: "INTERNAL_SERVER_ERROR",
+          message: "Error processing payment.",
+        });
       }
🧹 Nitpick comments (16)
tests/libs/__mocks__/app-store.ts (1)

4-5: Type-only import is fine here; no runtime cost.

Answering the FIXME: importing the “large type” via import type * as appStore won’t impact runtime since it erases at compile time. Safe for tests. If you begin mocking loader maps (e.g., paymentLoaders) elsewhere due to this PR, consider adding a dedicated mock for them too.

packages/app-store/_utils/createCachedImport.ts (1)

1-10: Avoid duplicate concurrent imports by caching the in-flight Promise.

Current logic can trigger multiple parallel imports if called concurrently before cachedModule is set. Cache the in-flight Promise and reset on failure.

Apply this diff:

-export function createCachedImport<T>(importFunc: () => Promise<T>): () => Promise<T> {
-  let cachedModule: T | undefined;
-
-  return async () => {
-    if (!cachedModule) {
-      cachedModule = await importFunc();
-    }
-    return cachedModule;
-  };
-}
+export function createCachedImport<T>(importFunc: () => Promise<T>): () => Promise<T> {
+  let cachedModule: T | undefined;
+  let inFlight: Promise<T> | null = null;
+
+  return async () => {
+    if (cachedModule) return cachedModule;
+    if (!inFlight) {
+      inFlight = importFunc()
+        .then((mod) => {
+          cachedModule = mod;
+          return mod;
+        })
+        .catch((err) => {
+          inFlight = null; // allow retry after failure
+          throw err;
+        });
+    }
+    return inFlight;
+  };
+}
packages/app-store-cli/src/build.ts (1)

349-367: Calendar metadata generation wiring LGTM; consider watching _metadata.ts for live rebuilds.

This mirrors the booker metadata block and filters via isCalendarApp; good. However, the watcher below only reacts to config.json changes. Since _metadata.ts is also a valid source, edits there won’t regenerate outputs in watch mode.

Suggested watcher addition (outside this hunk):

// in the chokidar "change" handler
.on("change", (filePath) => {
  if (filePath.endsWith("config.json") || filePath.endsWith("_metadata.ts")) {
    console.log("Config/metadata file changed");
    debouncedGenerateFiles();
  }
})

If you want, I can open a follow-up PR to add this change.

packages/app-store/_utils/getAnalytics.ts (2)

34-39: Avoid unsafe type assertion; guard the key and improve warning context.

Casting to AnalyticsLoadersKey can mask typos and lead to undefined loader calls. Prefer a runtime key guard and include the resolved name in the warning.

Apply this diff:

-  const analyticsAppImportFn = analyticsLoaders[analyticsName as AnalyticsLoadersKey];
+  const analyticsAppImportFn =
+    analyticsName in analyticsLoaders
+      ? analyticsLoaders[analyticsName as AnalyticsLoadersKey]
+      : undefined;
@@
-  if (!analyticsAppImportFn) {
-    log.warn(`analytics app not implemented`);
+  if (!analyticsAppImportFn) {
+    log.warn(`analytics app not implemented: ${analyticsName}`);
     return null;
   }

41-46: Wrap dynamic import in try/catch to avoid bubbling transient import errors.

A failed import (network/fs/transpile) will currently throw. Safely catch and log the analyticsName so the caller gets null instead of an unhandled rejection.

Suggested pattern:

-  const analyticsApp = await analyticsAppImportFn();
+  let analyticsApp: unknown;
+  try {
+    analyticsApp = await analyticsAppImportFn();
+  } catch (e) {
+    log.warn(`failed to import analytics app`, { analyticsName, error: (e as Error)?.message });
+    return null;
+  }
packages/app-store/_utils/getCalendar.ts (1)

48-48: Drop redundant optional chain and add try/catch around import.

You already guard when the loader is missing; optional chaining here hides type issues and swallows the signal. Prefer a try/catch to capture import errors and keep returning null.

-  const calendarApp = await calendarAppImportFn?.();
+  let calendarApp: unknown;
+  try {
+    calendarApp = await calendarAppImportFn();
+  } catch (e) {
+    log.warn(`failed to import calendar app for type ${calendarType}`, {
+      error: (e as Error)?.message,
+    });
+    return null;
+  }
packages/lib/payment/handlePaymentRefund.ts (2)

23-25: Improve warning context.

Interpolating the whole paymentApp object yields “[object Object]”. Log the dirName instead for actionable telemetry.

-    console.warn(`payment App service of type ${paymentApp} is not implemented`);
+    console.warn(`payment App service not implemented for dirName: ${paymentAppCredentials?.app?.dirName ?? "<unknown>"}`);

19-31: Optional: standardize refund error handling
We’ve confirmed that across all PaymentService implementations, refund() returns a Promise<Payment>, so any exception will bubble up. If you’d prefer to handle failed refunds gracefully:

  • Wrap the refund call in a try/catch block.
  • Log or transform the error.
  • Return a sentinel (e.g. false) instead of throwing.
  • Update the function signature to reflect Promise<Payment | false> (or Promise<boolean> if you only care about success/failure) and adjust callers accordingly.

Locations to update:

  • packages/lib/payment/handlePaymentRefund.ts:28–32

Example refactor:

- const refund = await paymentInstance.refund(paymentId);
- return refund;
+ try {
+   const refund = await paymentInstance.refund(paymentId);
+   return refund;
+ } catch (error) {
+   console.error('Refund failed for paymentId', paymentId, error);
+   return false;
+ }
packages/app-store/_utils/calendars/calendarLoaders.ts (1)

3-16: Solid centralization; consider stabilizing the map shape.

The loader registry is straightforward and will cache modules per createCachedImport. Recommend marking the object as const to preserve literal keys at the type level and prevent accidental mutation.

-export const calendarLoaders = {
+export const calendarLoaders = {
   applecalendar: createCachedImport(() => import("../../applecalendar")),
   caldavcalendar: createCachedImport(() => import("../../caldavcalendar")),
   googlecalendar: createCachedImport(() => import("../../googlecalendar")),
   "ics-feedcalendar": createCachedImport(() => import("../../ics-feedcalendar")),
   larkcalendar: createCachedImport(() => import("../../larkcalendar")),
   office365calendar: createCachedImport(() => import("../../office365calendar")),
   exchange2013calendar: createCachedImport(() => import("../../exchange2013calendar")),
   exchange2016calendar: createCachedImport(() => import("../../exchange2016calendar")),
   exchangecalendar: createCachedImport(() => import("../../exchangecalendar")),
   zohocalendar: createCachedImport(() => import("../../zohocalendar")),
-};
+} as const;

Note: With as const, CalendarLoaderKey remains keyof typeof calendarLoaders as desired.

packages/app-store/_utils/analytics/analyticsLoaders.ts (1)

3-8: Looks good; add as const to preserve literal key types.

Minor nit to further tighten typing and prevent accidental writes.

-export const analyticsLoaders = {
+export const analyticsLoaders = {
   dub: createCachedImport(() => import("../../dub")),
   plausible: createCachedImport(() => import("../../plausible")),
-};
+} as const;
packages/trpc/server/routers/viewer/payments.tsx (4)

112-115: Avoid ts-expect-error; add a safe, typed loader lookup with guard

Relying on @ts-expect-error hides real issues and can mask unsupported apps at runtime. Guard the key and drop the suppression.

Apply this diff:

-      // @ts-expect-error FIXME
-      const paymentApp = (await paymentLoaders[
-        paymentCredential?.app?.dirName as PaymentLoaderKey
-      ]?.()) as PaymentApp | null;
+      const dirName = paymentCredential.app.dirName as string;
+      if (!(dirName in paymentLoaders)) {
+        throw new TRPCError({ code: "BAD_REQUEST", message: "Unsupported payment app" });
+      }
+      const paymentApp = (await paymentLoaders[
+        dirName as keyof typeof paymentLoaders
+      ]()) as PaymentApp | null;

60-77: Parallelize attendee translations and fix variable misnaming

Translations are currently awaited sequentially inside the loop and the array is misnamed “attendeesListPromises” while holding objects. Use Promise.all over mapped promises and pass attendeesList[0] to the email.

Apply this diff:

-      const attendeesListPromises = [];
-
-      for (const attendee of booking.attendees) {
-        const attendeeObject = {
-          name: attendee.name,
-          email: attendee.email,
-          timeZone: attendee.timeZone,
-          language: {
-            translate: await getTranslation(attendee.locale ?? "en", "common"),
-            locale: attendee.locale ?? "en",
-          },
-        };
-
-        attendeesListPromises.push(attendeeObject);
-      }
-
-      const attendeesList = await Promise.all(attendeesListPromises);
+      const attendeesList = await Promise.all(
+        booking.attendees.map(async (attendee) => ({
+          name: attendee.name,
+          email: attendee.email,
+          timeZone: attendee.timeZone,
+          language: {
+            translate: await getTranslation(attendee.locale ?? "en", "common"),
+            locale: attendee.locale ?? "en",
+          },
+        }))
+      );

And:

-        await sendNoShowFeeChargedEmail(
-          attendeesListPromises[0],
-          evt,
-          booking?.eventType?.metadata as EventTypeMetadata
-        );
+        await sendNoShowFeeChargedEmail(
+          attendeesList[0],
+          evt,
+          booking?.eventType?.metadata as EventTypeMetadata
+        );

Also applies to: 148-153


23-26: Stricter input validation for bookingId

Minor improvement: ensure bookingId is a positive integer.

Apply this diff:

-      z.object({
-        bookingId: z.number(),
-      })
+      z.object({
+        bookingId: z.number().int().positive(),
+      })

30-47: Prefer Prisma select over include to avoid overfetching

Per repo guidelines, use select to fetch only needed fields. This reduces payload and CPU for serialization.

Apply this diff:

-      const booking = await prisma.booking.findUniqueOrThrow({
-        where: {
-          id: input.bookingId,
-        },
-        include: {
-          payment: true,
-          user: {
-            select: {
-              email: true,
-              locale: true,
-              name: true,
-              timeZone: true,
-            },
-          },
-          attendees: true,
-          eventType: true,
-        },
-      });
+      const booking = await prisma.booking.findUniqueOrThrow({
+        where: { id: input.bookingId },
+        select: {
+          id: true,
+          title: true,
+          startTime: true,
+          endTime: true,
+          eventTypeId: true,
+          payment: {
+            select: {
+              id: true,
+              appId: true,
+              amount: true,
+              currency: true,
+              paymentOption: true,
+              success: true,
+            },
+          },
+          user: {
+            select: {
+              email: true,
+              locale: true,
+              name: true,
+              timeZone: true,
+            },
+          },
+          attendees: {
+            select: {
+              name: true,
+              email: true,
+              timeZone: true,
+              locale: true,
+            },
+          },
+          eventType: {
+            select: {
+              slug: true,
+              customReplyToEmail: true,
+              metadata: true,
+            },
+          },
+        },
+      });
packages/app-store/_utils/payments/paymentLoaders.ts (1)

11-14: Remove ts-expect-error; define mock loader without mutating the inferred type

Avoid mutating the typed object with an undeclared key. Provide a stub when the mock is disabled so the shape stays consistent and no suppression is needed.

Apply this diff:

-export const paymentLoaders = {
+export const paymentLoaders = {
   alby: createCachedImport(() => import("../../alby")),
   paypal: createCachedImport(() => import("../../paypal")),
   stripepayment: createCachedImport(() => import("../../stripepayment")),
   hitpay: createCachedImport(() => import("../../hitpay")),
   btcpayserver: createCachedImport(() => import("../../btcpayserver")),
-};
-
-if (process.env.MOCK_PAYMENT_APP_ENABLED !== undefined) {
-  // @ts-expect-error FIXME
-  paymentLoaders["mock-payment-app"] = createCachedImport(() => import("../../mock-payment-app/index"));
-}
+  // Keep the key present to satisfy types; gate actual import behind flag
+  "mock-payment-app":
+    process.env.MOCK_PAYMENT_APP_ENABLED !== undefined
+      ? createCachedImport(() => import("../../mock-payment-app/index"))
+      : async () => {
+          throw new Error("Mock payment app not enabled");
+        },
+} as const;
 
-export type PaymentLoaderKey = keyof typeof paymentLoaders | "mock-payment-app";
+export type PaymentLoaderKey = keyof typeof paymentLoaders;

This preserves typings, removes the suppression, and prevents accidental undefined lookups at runtime.

Also applies to: 16-16

packages/app-store/_utils/calendars/getCalendarApps.ts (1)

51-57: Optional: Pre-index credentials to reduce repeated filters

You filter the full credentials array for each app (O(apps * credentials)). If the list grows, precompute a Map<appId, Credential[]>.

I can provide a concrete refactor if desired; it’s a small change that builds a lookup map once and then does O(1) accesses inside the reduce.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 48aa6bf and 5e7b3f0.

📒 Files selected for processing (19)
  • packages/app-store-cli/src/build.ts (4 hunks)
  • packages/app-store/_utils/analytics/analyticsLoaders.ts (1 hunks)
  • packages/app-store/_utils/calendars/calendarLoaders.ts (1 hunks)
  • packages/app-store/_utils/calendars/getCalendarApps.ts (1 hunks)
  • packages/app-store/_utils/createCachedImport.ts (1 hunks)
  • packages/app-store/_utils/getAnalytics.ts (2 hunks)
  • packages/app-store/_utils/getCalendar.ts (2 hunks)
  • packages/app-store/_utils/payments/paymentLoaders.ts (1 hunks)
  • packages/app-store/_utils/videos/videoLoaders.ts (1 hunks)
  • packages/app-store/calendarApps.metadata.generated.ts (1 hunks)
  • packages/lib/CalendarManager.ts (2 hunks)
  • packages/lib/getConnectedApps.ts (2 hunks)
  • packages/lib/payment/deletePayment.ts (2 hunks)
  • packages/lib/payment/handlePayment.ts (2 hunks)
  • packages/lib/payment/handlePaymentRefund.ts (2 hunks)
  • packages/lib/videoClient.ts (2 hunks)
  • packages/trpc/server/routers/viewer/payments.tsx (2 hunks)
  • packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts (2 hunks)
  • tests/libs/__mocks__/app-store.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.ts

📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)

**/*.ts: For Prisma queries, only select data you need; never use include, always use select
Ensure the credential.key field is never returned from tRPC endpoints or APIs

Files:

  • packages/lib/CalendarManager.ts
  • tests/libs/__mocks__/app-store.ts
  • packages/lib/videoClient.ts
  • packages/lib/getConnectedApps.ts
  • packages/app-store/_utils/videos/videoLoaders.ts
  • packages/app-store/_utils/getAnalytics.ts
  • packages/app-store/calendarApps.metadata.generated.ts
  • packages/app-store/_utils/createCachedImport.ts
  • packages/app-store/_utils/calendars/getCalendarApps.ts
  • packages/lib/payment/handlePayment.ts
  • packages/app-store/_utils/payments/paymentLoaders.ts
  • packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts
  • packages/app-store/_utils/analytics/analyticsLoaders.ts
  • packages/lib/payment/deletePayment.ts
  • packages/app-store/_utils/calendars/calendarLoaders.ts
  • packages/lib/payment/handlePaymentRefund.ts
  • packages/app-store/_utils/getCalendar.ts
  • packages/app-store-cli/src/build.ts
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)

Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js .utc() in hot paths like loops

Files:

  • packages/lib/CalendarManager.ts
  • tests/libs/__mocks__/app-store.ts
  • packages/lib/videoClient.ts
  • packages/lib/getConnectedApps.ts
  • packages/app-store/_utils/videos/videoLoaders.ts
  • packages/app-store/_utils/getAnalytics.ts
  • packages/app-store/calendarApps.metadata.generated.ts
  • packages/app-store/_utils/createCachedImport.ts
  • packages/app-store/_utils/calendars/getCalendarApps.ts
  • packages/lib/payment/handlePayment.ts
  • packages/trpc/server/routers/viewer/payments.tsx
  • packages/app-store/_utils/payments/paymentLoaders.ts
  • packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts
  • packages/app-store/_utils/analytics/analyticsLoaders.ts
  • packages/lib/payment/deletePayment.ts
  • packages/app-store/_utils/calendars/calendarLoaders.ts
  • packages/lib/payment/handlePaymentRefund.ts
  • packages/app-store/_utils/getCalendar.ts
  • packages/app-store-cli/src/build.ts
**/*.tsx

📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)

Always use t() for text localization in frontend code; direct text embedding should trigger a warning

Files:

  • packages/trpc/server/routers/viewer/payments.tsx
🧠 Learnings (1)
📚 Learning: 2025-08-05T12:04:29.037Z
Learnt from: din-prajapati
PR: calcom/cal.com#21854
File: packages/app-store/office365calendar/__tests__/unit_tests/SubscriptionManager.test.ts:0-0
Timestamp: 2025-08-05T12:04:29.037Z
Learning: In packages/app-store/office365calendar/lib/CalendarService.ts, the fetcher method in Office365CalendarService class is public, not private. It was specifically changed from private to public in this PR to support proper testing and external access patterns.

Applied to files:

  • packages/lib/CalendarManager.ts
  • packages/app-store/_utils/getCalendar.ts
🧬 Code Graph Analysis (16)
packages/lib/CalendarManager.ts (1)
packages/app-store/_utils/calendars/getCalendarApps.ts (1)
  • getCalendarApps (51-111)
packages/lib/videoClient.ts (1)
packages/app-store/_utils/videos/videoLoaders.ts (2)
  • videoLoaders (3-15)
  • VideoLoaderKey (17-17)
packages/lib/getConnectedApps.ts (1)
packages/app-store/_utils/payments/paymentLoaders.ts (2)
  • paymentLoaders (3-9)
  • PaymentLoaderKey (16-16)
packages/app-store/_utils/videos/videoLoaders.ts (1)
packages/app-store/_utils/createCachedImport.ts (1)
  • createCachedImport (1-10)
packages/app-store/_utils/getAnalytics.ts (1)
packages/app-store/_utils/analytics/analyticsLoaders.ts (2)
  • analyticsLoaders (3-6)
  • AnalyticsLoadersKey (8-8)
packages/app-store/calendarApps.metadata.generated.ts (1)
packages/app-store/_utils/calendars/getCalendarApps.ts (1)
  • calendarAppsMetadata (30-30)
packages/app-store/_utils/calendars/getCalendarApps.ts (3)
packages/app-store/locations.ts (1)
  • EventLocationType (58-58)
packages/app-store/calendarApps.metadata.generated.ts (1)
  • calendarAppsMetadata (20-35)
packages/lib/piiFreeData.ts (1)
  • getPiiFreeCredential (61-72)
packages/lib/payment/handlePayment.ts (1)
packages/app-store/_utils/payments/paymentLoaders.ts (1)
  • paymentLoaders (3-9)
packages/trpc/server/routers/viewer/payments.tsx (1)
packages/app-store/_utils/payments/paymentLoaders.ts (2)
  • paymentLoaders (3-9)
  • PaymentLoaderKey (16-16)
packages/app-store/_utils/payments/paymentLoaders.ts (1)
packages/app-store/_utils/createCachedImport.ts (1)
  • createCachedImport (1-10)
packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts (1)
packages/app-store/_utils/payments/paymentLoaders.ts (2)
  • paymentLoaders (3-9)
  • PaymentLoaderKey (16-16)
packages/app-store/_utils/analytics/analyticsLoaders.ts (1)
packages/app-store/_utils/createCachedImport.ts (1)
  • createCachedImport (1-10)
packages/lib/payment/deletePayment.ts (1)
packages/app-store/_utils/payments/paymentLoaders.ts (2)
  • paymentLoaders (3-9)
  • PaymentLoaderKey (16-16)
packages/app-store/_utils/calendars/calendarLoaders.ts (1)
packages/app-store/_utils/createCachedImport.ts (1)
  • createCachedImport (1-10)
packages/lib/payment/handlePaymentRefund.ts (1)
packages/app-store/_utils/payments/paymentLoaders.ts (2)
  • paymentLoaders (3-9)
  • PaymentLoaderKey (16-16)
packages/app-store/_utils/getCalendar.ts (1)
packages/app-store/_utils/calendars/calendarLoaders.ts (2)
  • CalendarLoaderKey (16-16)
  • calendarLoaders (3-14)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Install dependencies / Yarn install & cache
🔇 Additional comments (18)
packages/app-store-cli/src/build.ts (3)

41-41: Calendar metadata output collection added — looks consistent with existing patterns.


382-382: New generated file added to outputs.

Good inclusion; aligns with the new calendar app flow and consumers in calendar utilities.


431-433: Predicate aligns with category filtering elsewhere.

isCalendarApp mirrors isCrmApp and is straightforward.

packages/lib/payment/handlePayment.ts (1)

3-3: Switch to paymentLoaders aligns with the lazy-loader architecture.

Importing from @calcom/app-store/_utils/payments/paymentLoaders matches the new pattern; reduces eager imports and improves perf.

packages/lib/videoClient.ts (2)

4-5: LGTM! Clean import organization for the new video loader system.

The imports properly use named imports for the videoLoaders and type-only imports for VideoLoaderKey, following TypeScript best practices.


31-31: VideoLoaderKey coverage verified

All credential types ending in _video or _conferencing correctly map—via split("_").join("")—to keys in videoLoaders. No integrations are missing loader entries.

• daily_video → dailyvideo
• google_video → googlevideo
• huddle01_video → huddle01video
• shimmer_video → shimmervideo
• jitsi_video → jitsivideo
• webex_video → webexvideo
• sylaps_video → sylapsvideo
• office365_video → office365video
• tandem_video → tandemvideo
• zoom_video → zoomvideo
• jelly_conferencing → jellyconferencing

packages/lib/CalendarManager.ts (2)

4-4: LGTM! Clean import for the new calendar app utilities.

The import properly uses the new getCalendarApps utility from the calendar-specific loader system.


33-33: Verify getCalendarApps matches legacy getApps behavior

I wasn’t able to locate the original getApps implementation in the repo. Please manually confirm that switching to
getCalendarApps(credentials, true) preserves:

  • The same filterOnCredentials logic (skipping non-global apps with no credentials)
  • Credential handling (including the deprecated credential field alongside credentials)
  • The returned object shape (all fields previously returned by getApps)

Key locations to review:

  • packages/lib/CalendarManager.ts (import changed from getAppsgetCalendarApps)
  • packages/app-store/_utils/calendars/getCalendarApps.ts (filter and reducedArray.push structure)
packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts (1)

1-2: LGTM! Proper import structure for the new payment loader system.

The imports correctly bring in both the paymentLoaders map and the PaymentLoaderKey type, following the established pattern across the codebase.

packages/lib/getConnectedApps.ts (1)

4-5: LGTM! Consistent import pattern for payment loaders.

The imports follow the same pattern established in other files, using named imports for paymentLoaders and type-only imports for PaymentLoaderKey.

packages/lib/payment/deletePayment.ts (1)

3-4: LGTM! Consistent import pattern maintained.

The imports follow the established pattern used across all payment-related files in this PR.

packages/app-store/_utils/getAnalytics.ts (1)

5-6: Good move: switch to loader registry (cacheable, tree-shakeable).

Replacing appStore lookup with analyticsLoaders aligns with the new lazy-loading pattern and should reduce initial bundle/SSR cost. No issues here.

packages/app-store/_utils/getCalendar.ts (1)

5-6: Loader registry import looks correct.

Imports line up with the new calendarLoaders module and typed key. Good.

packages/lib/payment/handlePaymentRefund.ts (1)

3-4: Aligns with loader pattern.

Switch to paymentLoaders is consistent with the new lazy-loading approach. Good step towards decoupling and better SSR performance.

packages/trpc/server/routers/viewer/payments.tsx (2)

3-4: Good shift to lazy loaders for payment adapters

Switching to paymentLoaders aligns this module with the new lazy-load pattern and reduces cold-start overhead. Nice.


49-56: Guard against missing payment record

If booking.payment is empty, accessing payment.success will throw.

Apply this diff to avoid a runtime crash:

       const payment = booking.payment[0];
 
+      if (!payment) {
+        throw new TRPCError({
+          code: "BAD_REQUEST",
+          message: "No payment record found for this booking.",
+        });
+      }
+
       if (payment.success) {
         throw new TRPCError({
           code: "BAD_REQUEST",
           message: `The no show fee for ${booking.id} has already been charged.`,
         });
       }

If this case is impossible by design, please confirm and we can skip this guard.

packages/app-store/_utils/videos/videoLoaders.ts (1)

3-15: Solid addition; consistent with the new loader pattern

The loader map and cached dynamic imports look good and will minimize initial bundle cost.

packages/app-store/_utils/calendars/getCalendarApps.ts (1)

59-83: No client-side exposure of injected key
After tracing getCalendarApps usages:

  • It’s only consumed by getCalendarCredentials in packages/lib/CalendarManager.ts.
  • All returned objects go through getConnectedCalendars, which strips out full credential objects via cleanIntegrationKeys and only exposes credentialId (never the raw key).
  • Logging calls use getPiiFreeCredential, which redacts sensitive fields.
  • A grep across API and TRPC routes found no code that returns or serializes credential.key.

This synthetic credential’s key never reaches clients.

Comment on lines +40 to 42
const calendarLoaderKey = calendarType.split("_").join("") as CalendarLoaderKey;
const calendarAppImportFn = calendarLoaders[calendarLoaderKey];

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Bug: loader key derivation breaks for “ics-feedcalendar”.

calendarLoaders defines the ICS loader key as "ics-feedcalendar" (with a hyphen), but the current derivation removes underscores only, producing "icsfeedcalendar" which won’t match, causing ICS Feed calendars to fail to load.

Apply this minimal fix to handle the special case and harden the lookup:

-  const calendarLoaderKey = calendarType.split("_").join("") as CalendarLoaderKey;
-  const calendarAppImportFn = calendarLoaders[calendarLoaderKey];
+  // Derive loader key from type; handle special cases like ICS Feed
+  const baseKey = calendarType.split("_").join("");
+  const normalizedKey =
+    baseKey === "icsfeedcalendar" ? ("ics-feedcalendar" as const) : (baseKey as const);
+  const calendarAppImportFn =
+    normalizedKey in calendarLoaders
+      ? calendarLoaders[normalizedKey as CalendarLoaderKey]
+      : undefined;

Optionally, extract a reusable normalizer to avoid future key drift:

function toCalendarLoaderKey(t: string): CalendarLoaderKey | null {
  const base = t.replace(/_/g, "");
  const special: Record<string, CalendarLoaderKey> = { icsfeedcalendar: "ics-feedcalendar" };
  const resolved = (special[base] ?? base) as CalendarLoaderKey;
  return resolved in calendarLoaders ? resolved : null;
}
🤖 Prompt for AI Agents
In packages/app-store/_utils/getCalendar.ts around lines 40-42, the loader key
derivation currently strips only underscores and yields "icsfeedcalendar" which
doesn't match the defined "ics-feedcalendar" key; implement a small normalizer
function that removes underscores, maps the special-case base "icsfeedcalendar"
to "ics-feedcalendar", then check that the resolved key exists in
calendarLoaders before using it (return null or throw if not found). Replace the
inline split/join with a call to this normalizer to produce a valid
CalendarLoaderKey and use the normalized value for calendarLoaders lookup.

Comment on lines +1 to +35
/**
This file is autogenerated using the command `yarn app-store:build --watch`.
Don't modify this file manually.
**/
import amie_config_json from "./amie/config.json";
import { metadata as applecalendar__metadata_ts } from "./applecalendar/_metadata";
import { metadata as caldavcalendar__metadata_ts } from "./caldavcalendar/_metadata";
import cron_config_json from "./cron/config.json";
import { metadata as exchange2013calendar__metadata_ts } from "./exchange2013calendar/_metadata";
import { metadata as exchange2016calendar__metadata_ts } from "./exchange2016calendar/_metadata";
import exchangecalendar_config_json from "./exchangecalendar/config.json";
import { metadata as feishucalendar__metadata_ts } from "./feishucalendar/_metadata";
import { metadata as googlecalendar__metadata_ts } from "./googlecalendar/_metadata";
import ics_feedcalendar_config_json from "./ics-feedcalendar/config.json";
import { metadata as larkcalendar__metadata_ts } from "./larkcalendar/_metadata";
import { metadata as office365calendar__metadata_ts } from "./office365calendar/_metadata";
import vimcal_config_json from "./vimcal/config.json";
import zohocalendar_config_json from "./zohocalendar/config.json";

export const calendarAppsMetadata = {
amie: amie_config_json,
applecalendar: applecalendar__metadata_ts,
caldavcalendar: caldavcalendar__metadata_ts,
cron: cron_config_json,
exchange2013calendar: exchange2013calendar__metadata_ts,
exchange2016calendar: exchange2016calendar__metadata_ts,
exchangecalendar: exchangecalendar_config_json,
feishucalendar: feishucalendar__metadata_ts,
googlecalendar: googlecalendar__metadata_ts,
"ics-feedcalendar": ics_feedcalendar_config_json,
larkcalendar: larkcalendar__metadata_ts,
office365calendar: office365calendar__metadata_ts,
vimcal: vimcal_config_json,
zohocalendar: zohocalendar_config_json,
};
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Generated mapping looks correct; verify there’s no competing stub export.

The entries and imports are coherent. Ensure that getCalendarApps.ts (or any other module) no longer exports a stub calendarAppsMetadata = {} as CalendarAppsMetaData; which would diverge from this source of truth.

Run this to confirm there are no non-generated declarations:


🏁 Script executed:

#!/bin/bash
# Find other declarations of calendarAppsMetadata outside the generated file
rg -nP -C2 'export\s+const\s+calendarAppsMetadata\b' \
  -g '!**/calendarApps.metadata.generated.ts' --type=ts

Length of output: 636


Remove stub export of calendarAppsMetadata in getCalendarApps.ts and import the generated metadata instead

The file packages/app-store/_utils/calendars/getCalendarApps.ts still defines and exports a stubbed calendarAppsMetadata, which conflicts with the source of truth in calendarApps.metadata.generated.ts. Replace the manual stub with an import from the generated file:

• File: packages/app-store/_utils/calendars/getCalendarApps.ts
Lines: ~30–32

Suggested diff:

- export const calendarAppsMetadata = {} as CalendarAppsMetaData;
- for (const [key, value] of Object.entries(rawCalendarAppsMetadata)) {
-   calendarAppsMetadata[key as keyof typeof calendarAppsMetadata] = getNormalizedAppMetadata(value);
- }
+ import { calendarAppsMetadata } from '../../calendarApps.metadata.generated';

This ensures all consumers use the single, autogenerated mapping.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/app-store/_utils/calendars/getCalendarApps.ts around lines 30 to 32,
remove the manually defined/exported stubbed calendarAppsMetadata and replace it
by importing the autogenerated calendarAppsMetadata from
packages/app-store/calendarApps.metadata.generated.ts; update the top of the
file to add the import (e.g. import { calendarAppsMetadata } from
"packages/app-store/calendarApps.metadata.generated";) and delete the local
stub/export so all consumers use the single autogenerated mapping.

Comment on lines +19 to 22
// @ts-expect-error FIXME
const paymentApp = (await paymentLoaders[
paymentAppCredentials?.app?.dirName as PaymentLoaderKey
]?.()) as PaymentApp;
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Same TypeScript error pattern - consider consolidating the fix.

This is the third occurrence of the same @ts-expect-error pattern. Consider creating a helper function to handle payment app loading consistently across the codebase.

-  // @ts-expect-error FIXME
-  const paymentApp = (await paymentLoaders[
-    paymentAppCredentials?.app?.dirName as PaymentLoaderKey
-  ]?.()) as PaymentApp;
+  const dirName = paymentAppCredentials?.app?.dirName as PaymentLoaderKey;
+  const loader = paymentLoaders[dirName];
+  if (!loader) {
+    console.warn(`Payment app loader not found for ${dirName}`);
+    return false;
+  }
+  const paymentApp = (await loader()) as PaymentApp;

Consider creating a shared utility function for payment app loading:

// packages/lib/payment/loadPaymentApp.ts
export async function loadPaymentApp(dirName: string): Promise<PaymentApp | null> {
  const loader = paymentLoaders[dirName as PaymentLoaderKey];
  return loader ? (await loader()) as PaymentApp : null;
}
🤖 Prompt for AI Agents
In packages/lib/payment/deletePayment.ts around lines 19 to 22, the code uses a
repeated // @ts-expect-error pattern when invoking a dynamic payment loader;
consolidate this by creating a shared utility (e.g.,
packages/lib/payment/loadPaymentApp.ts) that accepts the dirName string, looks
up the loader with a PaymentLoaderKey cast, returns null if not found, and
otherwise awaits and returns the typed PaymentApp; then replace this direct
loader invocation with a call to that helper, add proper null checking and error
handling where the helper is used, and remove the inline @ts-expect-error
comments.

Comment on lines +55 to 60
if (!isKeyOf(paymentLoaders, key)) {
console.warn(`key: ${key} is not a valid key in appStore`);
return null;
}
const paymentApp = await appStore[key]?.();
const paymentApp = await paymentLoaders[key]?.();
if (!isPaymentApp(paymentApp)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Fix log message and consider tightening types and tests for the new loaders.

  • The warning still mentions “appStore”. Update message to reflect paymentLoaders.
  • Optional: use PaymentLoaderKey to narrow key at compile-time.
  • Tests that previously mocked @calcom/app-store may need to mock @calcom/app-store/_utils/payments/paymentLoaders now.

Apply this diff for the log message:

-  if (!isKeyOf(paymentLoaders, key)) {
-    console.warn(`key: ${key} is not a valid key in appStore`);
+  if (!isKeyOf(paymentLoaders, key)) {
+    console.warn(`key: ${key} is not a valid key in paymentLoaders`);
     return null;
   }

To mock the new loaders in Vitest (outside this file), you can add in your test setup:

vi.mock("@calcom/app-store/_utils/payments/paymentLoaders", () => ({
  paymentLoaders: {
    stripepayment: async () => ({ lib: { PaymentService: /* your mock */ {} as any } }),
    // ...other loaders as needed
  },
}));

Run this to find tests relying on the old app-store mock:


🏁 Script executed:

#!/bin/bash
# Search for tests mocking "@calcom/app-store"
rg -nP -C2 'vi\.mock\(\s*"@calcom/app-store"\s*,' --type=ts --type=tsx

Length of output: 100


🏁 Script executed:

#!/bin/bash
# Search for tests mocking "@calcom/app-store" in .ts and .tsx files
rg -nP -C2 'vi\.mock\(\s*"@calcom/app-store"' --glob '*.ts' --glob '*.tsx'

Length of output: 379


Fix warning message and update tests for paymentLoaders

  • Update the log in packages/lib/payment/handlePayment.ts to reference paymentLoaders instead of appStore.
  • Add a mock for the new module path in your Vitest setup so tests continue to pass.
  • Optional: tighten the key parameter to the PaymentLoaderKey type for compile-time safety.

Diff for the runtime code:

--- packages/lib/payment/handlePayment.ts
@@ lines 55-60
-  if (!isKeyOf(paymentLoaders, key)) {
-    console.warn(`key: ${key} is not a valid key in appStore`);
+  if (!isKeyOf(paymentLoaders, key)) {
+    console.warn(`key: ${key} is not a valid key in paymentLoaders`);
     return null;
   }

Example mock for Vitest (e.g. in tests/libs/__mocks__/paymentLoaders.ts or your global setup file):

vi.mock("@calcom/app-store/_utils/payments/paymentLoaders", () => ({
  paymentLoaders: {
    stripepayment: async () => ({
      lib: { PaymentService: {} as any },
    }),
    // …other loaders as needed
  },
}));

You’ll also want to remove or update the old mock in tests/libs/__mocks__/app-store.ts so it no longer intercepts the new loader import.

🤖 Prompt for AI Agents
In packages/lib/payment/handlePayment.ts around lines 55 to 60, the runtime
warning references the wrong module name ("appStore"); change the console warn
to reference paymentLoaders instead, and tighten the key parameter to
PaymentLoaderKey if possible. Update tests by adding a Vitest mock for the new
module path "@calcom/app-store/_utils/payments/paymentLoaders" which exports
paymentLoaders with the required loader entries, and remove or update the old
mock in tests/libs/__mocks__/app-store.ts so it no longer intercepts the loader
import; ensure the mock returns the same shape used by handlePayment to keep
tests passing.

Comment on lines +19 to 22
// @ts-expect-error FIXME
const paymentApp = (await paymentLoaders[
paymentAppCredentials?.app?.dirName as PaymentLoaderKey
]?.()) as PaymentApp;
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Remove ts-expect-error; add typed guard and clearer error when loader is missing.

The current assertion and // @ts-expect-error suppress real issues and can mask misconfigured app dirNames. Add a runtime key guard and avoid calling an undefined loader.

Apply this refactor:

-  // @ts-expect-error FIXME
-  const paymentApp = (await paymentLoaders[
-    paymentAppCredentials?.app?.dirName as PaymentLoaderKey
-  ]?.()) as PaymentApp;
+  const dirName = paymentAppCredentials?.app?.dirName;
+  if (!dirName || !(dirName in paymentLoaders)) {
+    console.warn(`payment loader not found for dirName: ${dirName ?? "<missing>"}`);
+    return false;
+  }
+  const loader = paymentLoaders[dirName as PaymentLoaderKey];
+  const paymentApp = (await loader?.()) as PaymentApp | undefined;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// @ts-expect-error FIXME
const paymentApp = (await paymentLoaders[
paymentAppCredentials?.app?.dirName as PaymentLoaderKey
]?.()) as PaymentApp;
const dirName = paymentAppCredentials?.app?.dirName;
if (!dirName || !(dirName in paymentLoaders)) {
console.warn(`payment loader not found for dirName: ${dirName ?? "<missing>"}`);
return false;
}
const loader = paymentLoaders[dirName as PaymentLoaderKey];
const paymentApp = (await loader?.()) as PaymentApp | undefined;
🤖 Prompt for AI Agents
In packages/lib/payment/handlePaymentRefund.ts around lines 19 to 22, remove the
// @ts-expect-error and the unchecked assertion; instead validate that
paymentAppCredentials?.app?.dirName is a valid PaymentLoaderKey present in
paymentLoaders before calling it, e.g. narrow the key with a typed guard (typeof
check or an Array.includes of Object.keys(paymentLoaders) cast to
PaymentLoaderKey), throw a clear runtime Error if the loader is missing
(including the problematic dirName and context), then call the loader and type
its result as PaymentApp only after the guard ensures it exists.

💡 Verification agent

🧩 Analysis chain

Verify loader coverage for payment app dirNames.

Ensure every payment app dirName in app.json is present in paymentLoaders to avoid runtime misses.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# List loader keys
PAYMENT_LOADERS_FILE="$(fd -a -t f 'paymentLoaders.ts' packages | head -n1)"
echo "Loader file: $PAYMENT_LOADERS_FILE"
echo "Loader keys:"
awk '
  /export const paymentLoaders/ { inObj=1; next }
  inObj && /};/ { inObj=0 }
  inObj && match($0, /^\s*([a-z0-9-]+):\s*createCachedImport/, m) { print m[1] }
' "$PAYMENT_LOADERS_FILE" | sort | tee /tmp/loader_keys.txt

# Extract payment app dirNames from app.json metadata
echo "Payment app dirNames from app.json:"
fd -a -t f app.json packages/app-store | xargs -I {} sh -c '
  if jq -e --arg cat "payment" '"'"'.categories | index($cat)'"'"' {} >/dev/null 2>&1; then
    jq -r ".dirName" {}
  fi
' | sort | tee /tmp/dirnames.txt

echo
echo "DirNames missing in loaders:"
comm -23 /tmp/dirnames.txt /tmp/loader_keys.txt || true

Length of output: 751


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the paymentLoaders file
PAYMENT_LOADERS_FILE=$(fd -t f 'paymentLoaders.ts' packages | head -n1)
echo "Loader file: $PAYMENT_LOADERS_FILE"

# Extract loader keys from the paymentLoaders object
awk '/export const paymentLoaders/,/};/' "$PAYMENT_LOADERS_FILE" \
  | grep -oP "^\s*['\"]?\K[a-z0-9-]+(?=['\"]?:\s*createCachedImport)" \
  | sort > /tmp/loader_keys.txt
echo "Loader keys:"
cat /tmp/loader_keys.txt

# Extract all payment app dirNames from app.json files
echo "Payment app dirNames from app.json:"
find packages/app-store -type f -name app.json -print0 \
  | xargs -0 jq -r 'select(.categories | index("payment")) | .dirName' \
  | sort > /tmp/dirnames.txt
cat /tmp/dirnames.txt

# Show any dirNames missing in the loaders
echo
echo "DirNames missing in loaders:"
comm -23 /tmp/dirnames.txt /tmp/loader_keys.txt || echo "None missing"

Length of output: 932


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "All JSON files in packages/app-store:"
fd -t f -e json packages/app-store || true

echo
echo "JSON files containing \"categories\":"
rg -n '"categories"' --glob 'packages/app-store/**/*.json' || true

echo
echo "JSON files containing \"dirName\":"
rg -n '"dirName"' --glob 'packages/app-store/**/*.json' || true

Length of output: 8420


Add missing loader for mock-payment-app

The paymentLoaders.ts registry currently includes:

  • alby
  • btcpayserver
  • hitpay
  • paypal
  • stripepayment

…but the only payment‐category apps in packages/app-store/**/config.json are:

  • alby
  • btcpayserver
  • hitpay
  • paypal
  • mock-payment-app

mock-payment-app is not registered, so paymentLoaders['mock-payment-app'] will be undefined at runtime.

• File to update:
packages/app-store/_utils/payments/paymentLoaders.ts

• Action: add an entry for mock-payment-app, e.g.:

 export const paymentLoaders = {
   alby:          createCachedImport(() => import(/* … */)),
   btcpayserver:  createCachedImport(() => import(/* … */)),
   hitpay:        createCachedImport(() => import(/* … */)),
   paypal:        createCachedImport(() => import(/* … */)),
   stripepayment: createCachedImport(() => import(/* … */)),
+  'mock-payment-app': createCachedImport(() => import(/* path to mock-payment-app payment adapter */)),
 };

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
packages/app-store/_utils/payments/paymentLoaders.ts (and note related runtime
use in packages/lib/payment/handlePaymentRefund.ts lines ~19-22): the registry
is missing an entry for "mock-payment-app" which causes
paymentLoaders['mock-payment-app'] to be undefined at runtime; add a loader
entry keyed exactly "mock-payment-app" following the existing pattern (import or
require the mock-payment-app loader module and add it to the exported
paymentLoaders object), ensuring the key matches the app config dirname and the
loader returns the same PaymentApp shape as the other entries.

Comment on lines +129 to 132
// @ts-expect-error FIXME
const paymentApp = (await paymentLoaders[
paymentCredential?.app?.dirName as PaymentLoaderKey
]?.()) as PaymentApp;
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Address the TypeScript error and improve type safety.

The @ts-expect-error comment indicates a type mismatch that should be resolved rather than suppressed. The issue likely stems from the optional chaining and type casting.

-  // @ts-expect-error FIXME
-  const paymentApp = (await paymentLoaders[
-    paymentCredential?.app?.dirName as PaymentLoaderKey
-  ]?.()) as PaymentApp;
+  const dirName = paymentCredential?.app?.dirName as PaymentLoaderKey;
+  const loader = paymentLoaders[dirName];
+  if (!loader) {
+    throw new TRPCError({ code: "BAD_REQUEST", message: "Payment app loader not found" });
+  }
+  const paymentApp = (await loader()) as PaymentApp;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// @ts-expect-error FIXME
const paymentApp = (await paymentLoaders[
paymentCredential?.app?.dirName as PaymentLoaderKey
]?.()) as PaymentApp;
const dirName = paymentCredential?.app?.dirName as PaymentLoaderKey;
const loader = paymentLoaders[dirName];
if (!loader) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Payment app loader not found" });
}
const paymentApp = (await loader()) as PaymentApp;
🤖 Prompt for AI Agents
In packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts around
lines 129-132, remove the @ts-expect-error and fix the type mismatch by
narrowing and guarding the loader key before calling it: extract
paymentCredential?.app?.dirName into a local const, add a runtime/type-guard
that asserts this value is a valid PaymentLoaderKey (or throw/return early if
missing), look up the loader from paymentLoaders with that guarded key, ensure
the loader exists, then await loader() and cast the result to PaymentApp;
alternatively, update the paymentLoaders type to include an index signature
mapping PaymentLoaderKey to a loader function so the lookup is statically
safe—either approach removes the need for @ts-expect-error and makes the call
type-safe.

Copy link
Contributor

@kart1ka kart1ka left a comment

Choose a reason for hiding this comment

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

A test seems to be failing. Can you please fix that?

@zhyd1997
Copy link
Contributor Author

A test seems to be failing. Can you please fix that?

Thanks, will do!

might need a little time

@graphite-app graphite-app bot requested a review from a team August 21, 2025 07:33
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
packages/app-store/_utils/calendars/getCalendarApps.ts (1)

83-99: Redact credential.key from returned data and fix nullable credential type

This function returns credentials including sensitive key. If any consumer exposes this via tRPC/API (e.g., /apps/installed/calendar), secrets could leak. Sanitize before returning and make credential nullable when no creds exist.

-    const credential: (typeof appCredentials)[number] | null = appCredentials[0] || null;
-
-    reducedArray.push({
+    // Remove sensitive fields like `key` before returning
+    const sanitizedCredentials = appCredentials.map(({ key, ...rest }) => rest as CredentialDataWithTeamName);
+    const credential: CredentialDataWithTeamName | null = sanitizedCredentials[0] || null;
+
+    reducedArray.push({
       ...appMeta,
       /**
        * @deprecated use `credentials`
        */
       credential,
-      credentials: appCredentials,
+      credentials: sanitizedCredentials,
       /** Option to display in `location` field while editing event types */
       locationOption,
     });
 
-    return reducedArray;
-  }, [] as (App & { credential: CredentialDataWithTeamName; credentials: CredentialDataWithTeamName[]; locationOption: LocationOption | null })[]);
+    return reducedArray;
+  }, [] as (App & {
+    credential: CredentialDataWithTeamName | null;
+    credentials: CredentialDataWithTeamName[];
+    locationOption: LocationOption | null;
+  })[]);
🧹 Nitpick comments (3)
packages/app-store/_utils/calendars/calendarAppsMetaData.ts (1)

11-14: Minor: build via Object.fromEntries to avoid intermediate mutation and tighten typing

Functionally equivalent but a bit cleaner and immutable.

-export const calendarAppsMetaData = {} as CalendarAppsMetaData;
-for (const [key, value] of Object.entries(rawCalendarAppsMetadata)) {
-  calendarAppsMetaData[key as keyof typeof calendarAppsMetaData] = getNormalizedAppMetadata(value);
-}
+export const calendarAppsMetaData = Object.fromEntries(
+  Object.entries(rawCalendarAppsMetadata).map(([key, value]) => [key, getNormalizedAppMetadata(value)])
+) as CalendarAppsMetaData;
packages/app-store/_utils/calendars/getCalendarApps.ts (1)

40-46: Optional: pre-index credentials by appId to reduce filter scans

Current approach is O(A*C). For large orgs, pre-bucket by appId to avoid repeated scans.

If interested, I can provide a small refactor that builds a Map<string, CredentialDataWithTeamName[]> once and then looks up per app.

apps/web/test/utils/bookingScenario/bookingScenario.ts (1)

1958-1965: ts-expect-error annotations are acceptable; ensure upstream mock provides adapter shape

The adapter surface is intentionally patched for tests. With the fixed videoAppsMock export shape (in tests/libs/mocks/videoApps.ts), this will work as intended.

If you want to avoid @ts-expect-error, we can declare a minimal interface for the mocked VideoApiAdapter in test types.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5e7b3f0 and f04a8fa.

📒 Files selected for processing (5)
  • apps/web/test/utils/bookingScenario/bookingScenario.ts (7 hunks)
  • packages/app-store/_utils/calendars/calendarAppsMetaData.ts (1 hunks)
  • packages/app-store/_utils/calendars/getCalendarApps.ts (1 hunks)
  • tests/libs/__mocks__/calendarApps.ts (1 hunks)
  • tests/libs/__mocks__/videoApps.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

**/*.ts: For Prisma queries, only select data you need; never use include, always use select
Ensure the credential.key field is never returned from tRPC endpoints or APIs

Files:

  • tests/libs/__mocks__/videoApps.ts
  • tests/libs/__mocks__/calendarApps.ts
  • packages/app-store/_utils/calendars/getCalendarApps.ts
  • packages/app-store/_utils/calendars/calendarAppsMetaData.ts
  • apps/web/test/utils/bookingScenario/bookingScenario.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js .utc() in hot paths like loops

Files:

  • tests/libs/__mocks__/videoApps.ts
  • tests/libs/__mocks__/calendarApps.ts
  • packages/app-store/_utils/calendars/getCalendarApps.ts
  • packages/app-store/_utils/calendars/calendarAppsMetaData.ts
  • apps/web/test/utils/bookingScenario/bookingScenario.ts
**/*.{ts,tsx,js,jsx}

⚙️ CodeRabbit configuration file

Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.

Files:

  • tests/libs/__mocks__/videoApps.ts
  • tests/libs/__mocks__/calendarApps.ts
  • packages/app-store/_utils/calendars/getCalendarApps.ts
  • packages/app-store/_utils/calendars/calendarAppsMetaData.ts
  • apps/web/test/utils/bookingScenario/bookingScenario.ts
🧠 Learnings (1)
📚 Learning: 2025-07-28T11:50:23.946Z
Learnt from: CR
PR: calcom/cal.com#0
File: .cursor/rules/review.mdc:0-0
Timestamp: 2025-07-28T11:50:23.946Z
Learning: Applies to **/*.ts : Ensure the `credential.key` field is never returned from tRPC endpoints or APIs

Applied to files:

  • packages/app-store/_utils/calendars/getCalendarApps.ts
🧬 Code graph analysis (1)
tests/libs/__mocks__/videoApps.ts (1)
packages/app-store/_utils/videos/videoLoaders.ts (1)
  • videoLoaders (3-15)
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Install dependencies / Yarn install & cache
  • GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (6)
tests/libs/__mocks__/videoApps.ts (1)

8-10: OK to reset in beforeEach; keep as-is

Resetting the deep mock per test is good practice and avoids cross-test pollution.

tests/libs/__mocks__/calendarApps.ts (1)

8-10: Good: reset mock per test

This ensures isolated scenarios.

apps/web/test/utils/bookingScenario/bookingScenario.ts (4)

2-6: Good migration to calendarAppsMetaData and new loader mocks

Imports now align with calendar-specific metadata and loader-based mocks for calendar/video. This keeps tests consistent with the new app-store loader architecture.

Also applies to: 16-16


1721-1774: Type-safety improvement on mockCalendar’s metadataLookupKey is spot on

Using keyof typeof calendarAppsMetaData prevents drift with real metadata keys. Logging and resolution via calendarAppsMetaData and calendarAppsMock are correct.


1156-1169: Note: getMockedCredential still sources from appStoreMetadata

This helper continues to use appStoreMetadata. That’s fine since it’s used across non-calendar mocks, but be aware it now diverges from the calendar-specific metadata source used elsewhere in this file.

Consider whether you want a calendar-specific helper mirroring calendarAppsMetaData for consistency.


957-988: Scenario orchestrator looks consistent after metadata/mocks shift

No issues noted with DB seeding and workflow wiring after the imports switch.

@zhyd1997 zhyd1997 marked this pull request as draft August 21, 2025 07:45
@zhyd1997 zhyd1997 marked this pull request as ready for review August 22, 2025 09:27
@zhyd1997 zhyd1997 requested review from a team and kart1ka August 22, 2025 09:27
@dosubot dosubot bot added the app-store area: app store, apps, calendar integrations, google calendar, outlook, lark, apple calendar label Aug 22, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/test/utils/bookingScenario/bookingScenario.ts (1)

1845-1847: Bug: returning id from normalizedCalendarData.update.uid (mixing uid/id)

In updateEvent, the returned id is sourced from normalizedCalendarData.update?.uid. That’s inconsistent with the create path (uses .create.id) and with the declared update shape that already has an id field. This can break assertions that compare id across create/update flows.

Apply this fix to prefer update.id, falling back to update.uid for backward compatibility:

-                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-                id: normalizedCalendarData.update?.uid || "FALLBACK_MOCK_ID",
+                id:
+                  normalizedCalendarData.update?.id ??
+                  normalizedCalendarData.update?.uid ??
+                  "FALLBACK_MOCK_ID",
🧹 Nitpick comments (17)
packages/app-store/_utils/videos/videoAppsMetaData.ts (3)

11-14: Avoid mutation and type assertions; build the map in a single typed pass

Current approach mutates a casted empty object and relies on type assertions. Build the map functionally to improve type safety and readability.

Apply this diff:

-export const videoAppsMetaData = {} as VideoAppsMetaData;
-for (const [key, value] of Object.entries(rawVideoAppsMetadata)) {
-  videoAppsMetaData[key as keyof typeof videoAppsMetaData] = getNormalizedAppMetadata(value);
-}
+export const videoAppsMetaData: VideoAppsMetaData = Object.entries(rawVideoAppsMetadata).reduce(
+  (acc, [key, value]) => {
+    acc[key as keyof RawVideoAppsMetaData] = getNormalizedAppMetadata(value);
+    return acc;
+  },
+  {} as VideoAppsMetaData
+);

6-9: Leverage “satisfies” to lock the shape without over-asserting

If getNormalizedAppMetadata guarantees dirName is a string, you can encode this with “satisfies” to validate the final object without hiding errors behind “as”.

Example (no behavior change, improves type checking):

type VideoAppsMetaData = {
  [key in keyof RawVideoAppsMetaData]: Omit<AppMeta, "dirName"> & { dirName: string };
};

Then ensure construction uses a value that satisfies VideoAppsMetaData (see previous comment). This helps catch discrepancies if the generator changes in the future.


1-14: Name consistency: MetaData vs. Metadata

Elsewhere the generated symbol/file is videoAppsMetadata (lowercase “d”). Consider renaming this file/symbol to videoAppsMetadata for consistency and easier discoverability.

packages/app-store/videoApps.metadata.generated.ts (1)

1-4: Autogenerated: no manual edits — ensure the builder watches _metadata.ts too

File looks good as generated output. Recommendation: in the builder, also trigger regeneration on changes to per-app _metadata.ts, not just config.json, to avoid stale outputs during local dev.

I’ve proposed a builder watch tweak in build.ts in a separate comment.

packages/app-store-cli/src/build.ts (1)

424-429: Watch _metadata.ts changes to keep generated outputs fresh

Currently regeneration only triggers on config.json changes. Apps using _metadata.ts won’t refresh in watch mode.

Apply this diff:

-    .on("change", (filePath) => {
-      if (filePath.endsWith("config.json")) {
-        console.log("Config file changed");
-        debouncedGenerateFiles();
-      }
-    })
+    .on("change", (filePath) => {
+      if (filePath.endsWith("config.json") || filePath.endsWith("_metadata.ts")) {
+        console.log(`${path.basename(filePath)} changed`);
+        debouncedGenerateFiles();
+      }
+    })
apps/api/v2/src/modules/conferencing/services/conferencing.service.ts (2)

97-99: Use Array.find for clarity and to avoid scanning the full list

filter()[0] is less clear and needlessly processes all items.

Apply this diff:

-    const foundApp = getVideoApps(credentials, true).filter((app) => app.slug === appSlug)[0];
+    const foundApp = getVideoApps(credentials, true).find((app) => app.slug === appSlug);

101-105: Add a defensive check that credential exists

After aligning types in getVideoApps (credential | null), we should assert presence here to keep type safety and avoid accidental undefined access.

Apply this diff:

-    if (!foundApp || !appLocation) {
+    if (!foundApp || !appLocation || !foundApp.credential) {
       throw new BadRequestException(`${appSlug} not connected.`);
     }
packages/lib/server/getDefaultLocations.ts (2)

23-27: Prefer .find over .filter(...)[0] for clarity and micro-optimization

.find expresses intent (single match) and avoids creating a temporary array.

-    const foundApp = getVideoApps(credentials, true).filter(
-      (app) => app.slug === defaultConferencingData.appSlug
-    )[0]; // There is only one possible install here so index [0] is the one we are looking for ;
+    const foundApp = getVideoApps(credentials, true).find(
+      (app) => app.slug === defaultConferencingData.appSlug
+    ); // There is only one possible install here

23-28: Avoid returning a stale link when the app slug is missing or not installed

If foundApp is undefined, we currently default the type to DailyLocationType but still return defaultConferencingData.appLink. That can mismatch the location type and the link. Consider falling back to Daily without a link when the app isn’t found.

-    const locationType = foundApp?.locationOption?.value ?? DailyLocationType; // Default to Daily if no location type is found
-    return [{ type: locationType, link: defaultConferencingData.appLink }];
+    if (!foundApp) {
+      // Slug not found or not installed: safe fallback to Daily without a stale link
+      return [{ type: DailyLocationType }];
+    }
+    const locationType = foundApp.locationOption?.value ?? DailyLocationType;
+    return [{ type: locationType, link: defaultConferencingData.appLink }];
packages/trpc/server/routers/viewer/apps/updateUserDefaultConferencingApp.handler.ts (1)

28-31: Use .find instead of .filter(...)[0]

Conveys uniqueness and avoids an intermediate array.

-  const foundApp = getVideoApps(credentials, true).filter((app) => app.slug === input.appSlug)[0];
+  const foundApp = getVideoApps(credentials, true).find((app) => app.slug === input.appSlug);
packages/lib/EventManager.ts (2)

154-166: Combine sorts into a single comparator to make ordering deterministic and avoid double traversal

Relying on sort stability across engines is usually okay in modern V8, but a single comparator is clearer and future-proof.

-      .sort(latestCredentialFirst)
-      // Keep Delegation Credentials first so because those credentials never expire and are preferred.
-      // Also, those credentials have consistent permission for all the members avoiding the scenario where user doesn't give all permissions
-      .sort(delegatedCredentialFirst);
+      // Delegation credentials first, then newest credential ID
+      .sort((a, b) => {
+        const byDelegation = delegatedCredentialFirst(a, b);
+        return byDelegation !== 0 ? byDelegation : latestCredentialFirst(a, b);
+      });

150-177: Minor perf nit: avoid flattening all app credentials when only CRM is needed

We still call getApps(...).flatMap(... ) to build appCredentials just to filter CRM/other_calendar later. If there’s no immediate need for per-app credential flattening outside CRM, consider deriving CRM credentials directly from user.credentials and mapping appName via a slug→name map. This reduces array allocations on hot paths.

-    const appCredentials = getApps(user.credentials, true).flatMap((app) =>
-      app.credentials.map((creds) => ({ ...creds, appName: app.name }))
-    );
+    const allApps = getApps(user.credentials, true);
+    const appNameBySlug = new Map(allApps.map((app) => [app.slug, app.name]));
@@
-    this.crmCredentials = appCredentials.filter(
-      (cred) => cred.type.endsWith("_crm") || cred.type.endsWith("_other_calendar")
-    );
+    this.crmCredentials = user.credentials
+      .filter((cred) => cred.type.endsWith("_crm") || cred.type.endsWith("_other_calendar"))
+      .map((cred) => ({ ...cred, appName: appNameBySlug.get(cred.appId || "") || "" }));
apps/web/test/utils/bookingScenario/bookingScenario.ts (5)

1772-1775: Indexing mocks: prefer explicit type cast helper to avoid brittle indexing

calendarAppsMock.default[appStoreLookupKey as keyof typeof calendarAppsMock.default] relies on a wide index signature and then feature-detects mockResolvedValue. Consider a tiny helper that narrows the loader to vi.Mock and fails loudly if not a mock function. This reduces the need for scattered ts-ignore/expect-error and yields cleaner diagnostics.

function getCalendarLoaderMock<K extends keyof typeof calendarAppsMock.default>(
  key: K
): vi.Mock {
  const loader = calendarAppsMock.default[key];
  if (typeof loader !== "function" || !("mockResolvedValue" in loader)) {
    throw new Error(`calendarAppsMock.default["${String(key)}"] is not a vi mock function`);
  }
  return loader as unknown as vi.Mock;
}

// usage:
const appLoaderMock = getCalendarLoaderMock(appStoreLookupKey as keyof typeof calendarAppsMock.default);
appLoaderMock.mockResolvedValue({ /* ... */ });

1959-1966: Replace ts-expect-error with explicit types to stabilize mocks

Two // @ts-expect-error FIXME comments leak here for videoAppsMock and VideoApiAdapter. Prefer minimal type assertions or a helper similar to the calendar one to avoid suppressing type checking. This keeps the mocks change-proof.

type VideoLoader = vi.Mock<Promise<{ lib: { VideoApiAdapter: (cred: unknown) => unknown } }>>;
const getVideoLoaderMock = (key: keyof typeof videoAppsMock.default) =>
  videoAppsMock.default[key] as unknown as VideoLoader;

// ...
getVideoLoaderMock(appStoreLookupKey).mockImplementation(() => Promise.resolve({
  lib: {
    VideoApiAdapter: (credential: unknown) => ({ /* ... */ }),
  },
}));

2069-2083: Payment mock: simplify Promise and avoid repeated ts-expect-error with a typed helper

Good move to route payments through paymentAppsMock. You can tighten this by:

  • using Promise.resolve(...) to reduce noise,
  • centralizing the type cast to a helper and dropping ts-expect-error.
-  paymentAppsMock.default[appStoreLookupKey as keyof typeof paymentAppsMock.default].mockImplementation(
-    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-    // @ts-expect-error FIXME
-    () => {
-      return new Promise((resolve) => {
-        resolve({
-          lib: {
-            // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-            // @ts-expect-error FIXME
-            PaymentService: MockPaymentService,
-          },
-        });
-      });
-    }
-  );
+  type PaymentLoader = vi.Mock<Promise<{ lib: { PaymentService: unknown } }>>;
+  const getPaymentLoaderMock = (k: keyof typeof paymentAppsMock.default) =>
+    paymentAppsMock.default[k] as unknown as PaymentLoader;
+  getPaymentLoaderMock(appStoreLookupKey).mockImplementation(() =>
+    Promise.resolve({ lib: { PaymentService: MockPaymentService } })
+  );

2099-2113: Unify to ts-expect-error and streamline implementation

This block still uses // @ts-ignore while nearby code uses // @ts-expect-error. Prefer ts-expect-error with a short justification so future type changes won’t hide errors. Also, use Promise.resolve(...) for brevity.

-  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-  //@ts-ignore
-  videoAppsMock.default[appStoreLookupKey].mockImplementation(() => {
-    return new Promise((resolve) => {
-      resolve({
+  // @ts-expect-error videoAppsMock default registry is a vi.Mock map at test-time
+  videoAppsMock.default[appStoreLookupKey].mockImplementation(() => {
+    return Promise.resolve({
         lib: {
           VideoApiAdapter: () => ({
             createMeeting: () => {
               throw new MockError("Error creating Video meeting");
             },
           }),
         },
-      });
-    });
+    });
   });

17-17: Add a runtime check for missing metadata entries

Although the generated metadata file (packages/app-store/calendarApps.metadata.generated.ts) exists and calendarAppsMetaData is populated at build time, it’s still possible for tests to run against an empty object if the raw metadata import isn’t bundled into the test build. Without a guard, looking up a missing key will return undefined, and subsequent property reads (e.g. app.type) will throw a less-informative “cannot read property ‘type’ of undefined” error.

Please add a defensive assertion immediately after you pull the entry from calendarAppsMetaData:

 import { calendarAppsMetaData } from "@calcom/app-store/_utils/calendars/calendarAppsMetaData";
@@
-  const app = calendarAppsMetaData[metadataLookupKey as keyof typeof calendarAppsMetaData];
+  const app = calendarAppsMetaData[metadataLookupKey as keyof typeof calendarAppsMetaData];
+  if (!app) {
+    throw new Error(
+      `Missing calendarAppsMetaData entry for "${String(metadataLookupKey)}". ` +
+      `Make sure calendar-apps metadata is generated and included in this test build.`
+    );
+  }

This will immediately surface a clear, actionable error when metadata isn’t present, rather than an obscure property-access failure later.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f04a8fa and 187ddcf.

📒 Files selected for processing (24)
  • apps/api/v2/src/modules/conferencing/services/conferencing.service.ts (3 hunks)
  • apps/web/test/utils/bookingScenario/bookingScenario.ts (9 hunks)
  • packages/app-store-cli/src/build.ts (4 hunks)
  • packages/app-store/_utils/calendars/calendarLoaders.ts (1 hunks)
  • packages/app-store/_utils/getCalendar.ts (2 hunks)
  • packages/app-store/_utils/payments/paymentLoaders.ts (1 hunks)
  • packages/app-store/_utils/videos/getVideoApps.ts (1 hunks)
  • packages/app-store/_utils/videos/videoAppsMetaData.ts (1 hunks)
  • packages/app-store/_utils/videos/videoLoaders.ts (1 hunks)
  • packages/app-store/videoApps.metadata.generated.ts (1 hunks)
  • packages/lib/CalendarManager.ts (2 hunks)
  • packages/lib/EventManager.ts (3 hunks)
  • packages/lib/getConnectedApps.ts (2 hunks)
  • packages/lib/payment/deletePayment.ts (2 hunks)
  • packages/lib/payment/handlePayment.ts (2 hunks)
  • packages/lib/payment/handlePaymentRefund.ts (2 hunks)
  • packages/lib/server/getDefaultLocations.ts (2 hunks)
  • packages/lib/videoClient.ts (2 hunks)
  • packages/trpc/server/routers/viewer/apps/updateUserDefaultConferencingApp.handler.ts (2 hunks)
  • packages/trpc/server/routers/viewer/payments.tsx (2 hunks)
  • packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts (2 hunks)
  • tests/libs/__mocks__/calendarApps.ts (1 hunks)
  • tests/libs/__mocks__/paymentApps.ts (1 hunks)
  • tests/libs/__mocks__/videoApps.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (14)
  • packages/lib/CalendarManager.ts
  • packages/app-store/_utils/calendars/calendarLoaders.ts
  • packages/lib/payment/deletePayment.ts
  • packages/app-store/_utils/payments/paymentLoaders.ts
  • packages/lib/videoClient.ts
  • tests/libs/mocks/videoApps.ts
  • packages/lib/payment/handlePaymentRefund.ts
  • packages/trpc/server/routers/viewer/payments.tsx
  • packages/app-store/_utils/videos/videoLoaders.ts
  • tests/libs/mocks/calendarApps.ts
  • packages/lib/payment/handlePayment.ts
  • packages/lib/getConnectedApps.ts
  • packages/app-store/_utils/getCalendar.ts
  • packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts
🧰 Additional context used
📓 Path-based instructions (4)
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

**/*.ts: For Prisma queries, only select data you need; never use include, always use select
Ensure the credential.key field is never returned from tRPC endpoints or APIs

Files:

  • packages/app-store/_utils/videos/videoAppsMetaData.ts
  • packages/app-store/videoApps.metadata.generated.ts
  • apps/api/v2/src/modules/conferencing/services/conferencing.service.ts
  • packages/lib/server/getDefaultLocations.ts
  • packages/trpc/server/routers/viewer/apps/updateUserDefaultConferencingApp.handler.ts
  • tests/libs/__mocks__/paymentApps.ts
  • packages/app-store/_utils/videos/getVideoApps.ts
  • packages/lib/EventManager.ts
  • apps/web/test/utils/bookingScenario/bookingScenario.ts
  • packages/app-store-cli/src/build.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js .utc() in hot paths like loops

Files:

  • packages/app-store/_utils/videos/videoAppsMetaData.ts
  • packages/app-store/videoApps.metadata.generated.ts
  • apps/api/v2/src/modules/conferencing/services/conferencing.service.ts
  • packages/lib/server/getDefaultLocations.ts
  • packages/trpc/server/routers/viewer/apps/updateUserDefaultConferencingApp.handler.ts
  • tests/libs/__mocks__/paymentApps.ts
  • packages/app-store/_utils/videos/getVideoApps.ts
  • packages/lib/EventManager.ts
  • apps/web/test/utils/bookingScenario/bookingScenario.ts
  • packages/app-store-cli/src/build.ts
**/*.{ts,tsx,js,jsx}

⚙️ CodeRabbit configuration file

Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.

Files:

  • packages/app-store/_utils/videos/videoAppsMetaData.ts
  • packages/app-store/videoApps.metadata.generated.ts
  • apps/api/v2/src/modules/conferencing/services/conferencing.service.ts
  • packages/lib/server/getDefaultLocations.ts
  • packages/trpc/server/routers/viewer/apps/updateUserDefaultConferencingApp.handler.ts
  • tests/libs/__mocks__/paymentApps.ts
  • packages/app-store/_utils/videos/getVideoApps.ts
  • packages/lib/EventManager.ts
  • apps/web/test/utils/bookingScenario/bookingScenario.ts
  • packages/app-store-cli/src/build.ts
**/*.{service,repository}.ts

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Avoid dot-suffixes like .service.ts or .repository.ts for new files; reserve .test.ts, .spec.ts, .types.ts for their specific purposes

Files:

  • apps/api/v2/src/modules/conferencing/services/conferencing.service.ts
🧬 Code graph analysis (6)
apps/api/v2/src/modules/conferencing/services/conferencing.service.ts (1)
packages/app-store/_utils/videos/getVideoApps.ts (1)
  • getVideoApps (40-100)
packages/lib/server/getDefaultLocations.ts (1)
packages/app-store/_utils/videos/getVideoApps.ts (1)
  • getVideoApps (40-100)
packages/trpc/server/routers/viewer/apps/updateUserDefaultConferencingApp.handler.ts (1)
packages/app-store/_utils/videos/getVideoApps.ts (1)
  • getVideoApps (40-100)
packages/app-store/_utils/videos/getVideoApps.ts (3)
packages/app-store/locations.ts (1)
  • EventLocationType (58-58)
packages/app-store/_utils/videos/videoAppsMetaData.ts (1)
  • videoAppsMetaData (11-11)
packages/lib/piiFreeData.ts (1)
  • getPiiFreeCredential (61-72)
packages/lib/EventManager.ts (2)
packages/app-store/_utils/calendars/getCalendarApps.ts (1)
  • getCalendarApps (40-100)
packages/app-store/_utils/videos/getVideoApps.ts (1)
  • getVideoApps (40-100)
apps/web/test/utils/bookingScenario/bookingScenario.ts (1)
packages/app-store/_utils/calendars/calendarAppsMetaData.ts (1)
  • calendarAppsMetaData (11-11)
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (15)
packages/app-store-cli/src/build.ts (2)

350-388: Calendar/video metadata generation: nice addition, aligns with goals

The two new generated collections (calendarAppsMetadata, videoAppsMetadata) are wired correctly through getExportedObject and filtered by isCalendarApp/isVideoApp. This should help trim import graphs and improve build times on the installed apps pages.


457-459: Category predicate covers both “video” and “conferencing”

Good call including “conferencing” for legacy categorization.

packages/app-store/_utils/videos/getVideoApps.ts (1)

40-100: No credential.key exposed in API responses

I’ve verified all getVideoApps call sites and confirmed that none of the API or tRPC handlers return the raw credentials (including key) to clients:

  • In packages/trpc/server/routers/viewer/apps/updateUserDefaultConferencingApp.handler.ts, the output is limited to the selected app slug and location, not the credential payload.
  • In packages/lib/server/getDefaultLocations.ts and the NestJS service at apps/api/v2/src/modules/conferencing/services/conferencing.service.ts, we only derive and return location data.
  • All other usages (e.g., in packages/lib/EventManager.ts) occur within backend-only libraries that decrypt or process the key internally.

No further sanitization is required at these call sites.

apps/api/v2/src/modules/conferencing/services/conferencing.service.ts (2)

17-17: Switch to getVideoApps: aligns with new metadata/loader flow

Import change is correct and aligns this service with the new video loader/metadata pipeline.


107-114: Delete handler import integration looks good

handleDeleteCredential usage is straightforward; once the credential nullability guard is in place, this path remains safe.

packages/lib/server/getDefaultLocations.ts (1)

2-2: Good swap to category-specific loader (video) — aligns with new loader architecture

Using getVideoApps here is correct and keeps this module scoped to video apps only. It also benefits from global-app credential injection and avoids scanning unrelated app categories.

packages/trpc/server/routers/viewer/apps/updateUserDefaultConferencingApp.handler.ts (1)

3-3: Right import — use video-only catalog for conferencing defaults

Switching to getVideoApps keeps this handler focused on video apps and leverages global-app credential injection. Nice.

packages/lib/EventManager.ts (2)

7-10: Scoped imports to calendar/video loaders are the right direction

Adopting getCalendarApps and getVideoApps reduces cross-category work and keeps credentials derivation aligned with the new metadata/loader split.


168-174: Video credentials derivation looks good; filter and sort capture the right set

Using getVideoApps + suffix filters is consistent with how video types are named, and newest-first sorting matches “latest connection wins”.

tests/libs/__mocks__/paymentApps.ts (2)

1-1: Nice: global reset and explicit failure fallback

Resetting the deep mock in beforeEach and throwing loudly via fallbackMockImplementation are good testing ergonomics. This will make missing mocks fail fast.

Also applies to: 8-10, 20-20


12-20: Mock shape matches the real paymentLoaders export
I’ve confirmed that the real module at packages/app-store/_utils/payments/paymentLoaders.ts:

  • Exports a default object whose keys are exactly alby, paypal, stripepayment, hitpay, btcpayserver (and, when MOCK_PAYMENT_APP_ENABLED is set, adds "mock-payment-app").
  • Each key’s value is a loader function (returned by createCachedImport), so under a deep mock each becomes a jest.Mock and supports .mockImplementation(...).

Since mockDeep<typeof paymentLoaders> correctly mirrors that shape, no changes to the mock are needed.

apps/web/test/utils/bookingScenario/bookingScenario.ts (4)

1723-1723: Improved typing for mockCalendar key

Switching to keyof typeof calendarAppsMetaData strengthens call-site safety. Good change.


1906-1918: Helper overload updates look good

The new signatures keyed by keyof typeof calendarAppsMetaData align with mockCalendar and improve type safety across helpers.


1766-1766: Good: targeted log context

Adding the app key to the log line helps chase down the specific mocked adapter in test traces.


2-6: Mocks imports verified – please confirm Vitest path resolution

All three mock files exist and your relative imports resolve correctly from apps/web/test/utils/bookingScenario:

  • tests/libs/mocks/calendarApps.ts
  • tests/libs/mocks/videoApps.ts
  • tests/libs/mocks/paymentApps.ts

Please verify that your Vitest configuration (e.g. resolve.alias, root, or test.include) supports these long relative paths when tests are executed from different working directories. Alternatively, consider adding a path alias (for example, @tests<root>/tests) in your Vitest config to simplify and stabilize mock imports.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

♻️ Duplicate comments (1)
packages/app-store/_utils/sanitizeAppsMetadata.ts (1)

3-19: Avoid mutation + ts-ignore; create a cleaned copy instead

Deleting magic keys with ts-ignore is brittle and mutates imported metadata. Build a sanitized shallow copy using destructuring to keep typing sound and avoid side effects.

Apply:

 export const sanitizeAppsMetadata = <T extends object>(appsMetaData: T) => {
   const appsMap = Object.keys(appsMetaData).reduce((store, key) => {
-    const metadata = appsMetaData[key as keyof T] as AppMeta;
-
-    store[key] = metadata;
-
-    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-    //@ts-ignore
-    delete store[key]["/*"];
-    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-    //@ts-ignore
-    delete store[key]["__createdUsingCli"];
-    return store;
+    const metadata = appsMetaData[key as keyof T] as AppMeta & {
+      ["/*"]?: unknown;
+      __createdUsingCli?: unknown;
+    };
+    // Create a cleaned copy without internal markers
+    const { ["/*"]: _omitInternal, __createdUsingCli: _omitCli, ...cleaned } = metadata;
+    store[key] = cleaned;
+    return store;
   }, {} as Record<string, AppMeta>);
 
   return Object.values(appsMap);
 };
🧹 Nitpick comments (5)
packages/app-store/_utils/prepareAppsWithCredentials.ts (1)

36-52: Nit: Sentinel id 0 for synthetic credentials

Using id: 0 risks colliding with real records if any system treats 0 as “valid” rather than “absent”. Consider a negative sentinel or a branded type to avoid ambiguity.

packages/app-store/_utils/sanitizeAppsMetadata.ts (1)

1-1: Optional: make return type explicit

For readability and editor inference, consider annotating the return type as AppMeta[].

packages/app-store/utils.ts (3)

22-35: Avoid mutating ALL_APPS_MAP; use sanitized copies

This block deletes fields from imported metadata with ts-ignore, mutating shared objects. Prefer deriving this data from ALL_APPS (already sanitized) or clone first.

For example:

// Derive by slug from sanitized array
const ALL_APPS_BY_SLUG = new Map(ALL_APPS.map((a) => [a.slug, a] as const));

export function getAppName(name: string): string | null {
  return ALL_APPS_BY_SLUG.get(name)?.name ?? null;
}

export function getAppType(name: string): string {
  const type = ALL_APPS_BY_SLUG.get(name)?.type;
  if (!type) return "Unknown";
  if (type.endsWith("_calendar")) return "Calendar";
  if (type.endsWith("_payment")) return "Payment";
  return "Unknown";
}

122-123: Prefer named exports over default for utilities

Default exports make refactors harder and hurt tree-shaking. Consider export { getApps } and update imports.


36-40: Type duplication: reuse shared CredentialDataWithTeamName

CredentialDataWithTeamName is exported from ./_utils/prepareAppsWithCredentials. Import that type here instead of re-declaring to avoid drift.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 187ddcf and df97c69.

📒 Files selected for processing (5)
  • packages/app-store/_utils/calendars/getCalendarApps.ts (1 hunks)
  • packages/app-store/_utils/prepareAppsWithCredentials.ts (1 hunks)
  • packages/app-store/_utils/sanitizeAppsMetadata.ts (1 hunks)
  • packages/app-store/_utils/videos/getVideoApps.ts (1 hunks)
  • packages/app-store/utils.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

**/*.ts: For Prisma queries, only select data you need; never use include, always use select
Ensure the credential.key field is never returned from tRPC endpoints or APIs

Files:

  • packages/app-store/_utils/videos/getVideoApps.ts
  • packages/app-store/_utils/sanitizeAppsMetadata.ts
  • packages/app-store/_utils/calendars/getCalendarApps.ts
  • packages/app-store/utils.ts
  • packages/app-store/_utils/prepareAppsWithCredentials.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js .utc() in hot paths like loops

Files:

  • packages/app-store/_utils/videos/getVideoApps.ts
  • packages/app-store/_utils/sanitizeAppsMetadata.ts
  • packages/app-store/_utils/calendars/getCalendarApps.ts
  • packages/app-store/utils.ts
  • packages/app-store/_utils/prepareAppsWithCredentials.ts
**/*.{ts,tsx,js,jsx}

⚙️ CodeRabbit configuration file

Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.

Files:

  • packages/app-store/_utils/videos/getVideoApps.ts
  • packages/app-store/_utils/sanitizeAppsMetadata.ts
  • packages/app-store/_utils/calendars/getCalendarApps.ts
  • packages/app-store/utils.ts
  • packages/app-store/_utils/prepareAppsWithCredentials.ts
🧠 Learnings (1)
📚 Learning: 2025-07-28T11:50:23.946Z
Learnt from: CR
PR: calcom/cal.com#0
File: .cursor/rules/review.mdc:0-0
Timestamp: 2025-07-28T11:50:23.946Z
Learning: Applies to **/*.ts : Ensure the `credential.key` field is never returned from tRPC endpoints or APIs

Applied to files:

  • packages/app-store/_utils/videos/getVideoApps.ts
  • packages/app-store/_utils/calendars/getCalendarApps.ts
🧬 Code graph analysis (4)
packages/app-store/_utils/videos/getVideoApps.ts (3)
packages/app-store/_utils/sanitizeAppsMetadata.ts (1)
  • sanitizeAppsMetadata (3-19)
packages/app-store/_utils/videos/videoAppsMetaData.ts (1)
  • videoAppsMetaData (11-11)
packages/app-store/_utils/prepareAppsWithCredentials.ts (2)
  • CredentialDataWithTeamName (16-20)
  • prepareAppsWithCredentials (22-84)
packages/app-store/_utils/calendars/getCalendarApps.ts (3)
packages/app-store/_utils/sanitizeAppsMetadata.ts (1)
  • sanitizeAppsMetadata (3-19)
packages/app-store/_utils/calendars/calendarAppsMetaData.ts (1)
  • calendarAppsMetaData (11-11)
packages/app-store/_utils/prepareAppsWithCredentials.ts (2)
  • CredentialDataWithTeamName (16-20)
  • prepareAppsWithCredentials (22-84)
packages/app-store/utils.ts (2)
packages/app-store/_utils/sanitizeAppsMetadata.ts (1)
  • sanitizeAppsMetadata (3-19)
packages/app-store/_utils/prepareAppsWithCredentials.ts (2)
  • CredentialDataWithTeamName (16-20)
  • prepareAppsWithCredentials (22-84)
packages/app-store/_utils/prepareAppsWithCredentials.ts (2)
packages/app-store/locations.ts (1)
  • EventLocationType (61-61)
packages/lib/piiFreeData.ts (1)
  • getPiiFreeCredential (61-72)
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Tests / Unit
  • GitHub Check: Type check / check-types
🔇 Additional comments (2)
packages/app-store/utils.ts (2)

10-12: Good move delegating to shared utilities

Switching to prepareAppsWithCredentials and sanitizeAppsMetadata removes duplication and centralizes behavior.


42-42: Nice: centralized metadata sanitation

Using sanitizeAppsMetadata for ALL_APPS keeps internal markers out of public consumption.

Comment on lines +8 to +10
export const getCalendarApps = (credentials: CredentialDataWithTeamName[], filterOnCredentials?: boolean) => {
return prepareAppsWithCredentials(ALL_CALENDAR_APPS, credentials, filterOnCredentials);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Redact credential.key before returning calendar apps

Aligns with security guidance and avoids leaking secrets to the UI.

Apply:

-export const getCalendarApps = (credentials: CredentialDataWithTeamName[], filterOnCredentials?: boolean) => {
-  return prepareAppsWithCredentials(ALL_CALENDAR_APPS, credentials, filterOnCredentials);
-};
+export const getCalendarApps = (
+  credentials: CredentialDataWithTeamName[],
+  filterOnCredentials?: boolean
+) => {
+  const apps = prepareAppsWithCredentials(ALL_CALENDAR_APPS, credentials, filterOnCredentials);
+  return apps.map(({ credentials: creds, ...app }) => {
+    const sanitizedCredentials: Omit<CredentialDataWithTeamName, "key">[] = creds.map(
+      ({ key, ...rest }) => rest
+    );
+    return {
+      ...app,
+      credentials: sanitizedCredentials,
+      credential: sanitizedCredentials[0] ?? null,
+    };
+  });
+};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const getCalendarApps = (credentials: CredentialDataWithTeamName[], filterOnCredentials?: boolean) => {
return prepareAppsWithCredentials(ALL_CALENDAR_APPS, credentials, filterOnCredentials);
};
export const getCalendarApps = (
credentials: CredentialDataWithTeamName[],
filterOnCredentials?: boolean
) => {
const apps = prepareAppsWithCredentials(
ALL_CALENDAR_APPS,
credentials,
filterOnCredentials
);
return apps.map(({ credentials: creds, ...app }) => {
const sanitizedCredentials: Omit<CredentialDataWithTeamName, "key">[] =
creds.map(({ key, ...rest }) => rest);
return {
...app,
credentials: sanitizedCredentials,
credential: sanitizedCredentials[0] ?? null,
};
});
};
🤖 Prompt for AI Agents
In packages/app-store/_utils/calendars/getCalendarApps.ts around lines 8 to 10,
the function currently returns apps with credential.key values that could leak
secrets to the UI; before returning, remove or redact credential.key for every
credential attached to the prepared apps (e.g., map over the result of
prepareAppsWithCredentials and replace credential.key with a masked string or
delete the key property), ensuring the returned objects preserve other
credential metadata but never include the raw secret.

Comment on lines +22 to +26
export const prepareAppsWithCredentials = (
apps: App[],
credentials: CredentialDataWithTeamName[],
filterOnCredentials?: boolean
) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Confirm public exposure and redact credential.key before returning outside server boundaries

This helper returns credentials including key. If any consumer sends this result through tRPC/API (e.g., powering /apps/installed/* pages), it would leak secrets. Either sanitize here or at the module boundary (e.g., in getCalendarApps/getVideoApps/getApps).

Run to locate potential API exposures:

If exposure is confirmed, I can provide a minimal patch to sanitize at the call sites or here with a feature flag (e.g., sanitizeSecrets = true).


🏁 Script executed:

#!/bin/bash
# Find callers of these helpers in routers/APIs and inspect returned fields
rg -nP -C3 --type=ts '\b(getApps|getCalendarApps|getVideoApps)\s*\('

# Heuristic: look for tRPC/router handlers returning "credentials" or "credential"
rg -nP -C3 --type=ts '(createTRPCRouter|publicProcedure|privateProcedure|router\(|NextResponse\.json|res\.json)\b'

# Check for accidental exposure of credential.key
rg -nP -C2 --type=ts '\bcredential\.key\b|credentials?:\s*\[?[^{\]]*key'

Length of output: 182599


🏁 Script executed:

#!/bin/bash
# Display the implementation of prepareAppsWithCredentials to inspect credential handling
sed -n '1,200p' packages/app-store/_utils/prepareAppsWithCredentials.ts

Length of output: 2716


Redact credential.key in prepareAppsWithCredentials before exposing to clients

prepareAppsWithCredentials currently embeds each credential’s raw key in both the credential and credentials arrays. Any consumer (e.g. via getApps → front‐end pages or API routers) will inherit those secrets unless explicitly stripped.

Please update as follows:

• File: packages/app-store/_utils/prepareAppsWithCredentials.ts
– After building appCredentials, map each through getPiiFreeCredential (or explicitly delete the key field) before pushing.
– Example patch:

 …  
   const credential: CredentialDataWithTeamName | null = appCredentials[0] || null;
-    reducedArray.push({
-      ...appMeta,
-      credential,
-      credentials: appCredentials,
+    // remove `key` from any outgoing credential
+    const safeCredentials = appCredentials.map(getPiiFreeCredential);
+    const safeCredential = safeCredentials[0] || null;
+    reducedArray.push({
+      ...appMeta,
+      credential: safeCredential,
+      credentials: safeCredentials,
    locationOption,
  });

• Verify all call sites (e.g. any tRPC procedures or REST handlers invoking getApps/getCalendarApps/getVideoApps) do not forward the raw key. Either sanitize at prepareAppsWithCredentials or at those boundaries.
• Optionally, consider renaming the deprecated credential field or splitting into a “public” DTO to avoid accidental leaks.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const prepareAppsWithCredentials = (
apps: App[],
credentials: CredentialDataWithTeamName[],
filterOnCredentials?: boolean
) => {
const credential: CredentialDataWithTeamName | null = appCredentials[0] || null;
// remove `key` from any outgoing credential
const safeCredentials = appCredentials.map(getPiiFreeCredential);
const safeCredential = safeCredentials[0] || null;
reducedArray.push({
...appMeta,
- credential,
credential: safeCredential,
credentials: safeCredentials,
locationOption,
});
🤖 Prompt for AI Agents
In packages/app-store/_utils/prepareAppsWithCredentials.ts around lines 22–26,
the function currently includes raw credential.key values in returned objects;
update the code so that before pushing appCredentials into the
credential/credentials arrays you map each credential through
getPiiFreeCredential (or explicitly delete the key property) to remove the
secret, ensuring no raw keys are exposed to clients; additionally audit and
confirm all call sites (tRPC procedures, REST handlers like
getApps/getCalendarApps/getVideoApps) do not forward raw credential.key values
and consider renaming or splitting the deprecated credential field into a
sanitized public DTO to prevent accidental leaks.

Comment on lines +35 to +58
if (appMeta.isGlobal) {
const credential = {
id: 0,
type: appMeta.type,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
key: appMeta.key!,
userId: 0,
user: { email: "" },
teamId: null,
appId: appMeta.slug,
invalid: false,
delegatedTo: null,
delegatedToId: null,
delegationCredentialId: null,
team: {
name: "Global",
},
};
logger.debug(
`${appMeta.type} is a global app, injecting credential`,
safeStringify(getPiiFreeCredential(credential))
);
appCredentials.push(credential);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Prevent duplicate global credentials and guard missing key

Only inject the synthetic global credential when none exists for this app, and guard against a missing appMeta.key to avoid runtime assertion with !.

Apply:

-    /** If the app is a globally installed one, let's inject it's key */
-    if (appMeta.isGlobal) {
-      const credential = {
-        id: 0,
-        type: appMeta.type,
-        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        key: appMeta.key!,
-        userId: 0,
-        user: { email: "" },
-        teamId: null,
-        appId: appMeta.slug,
-        invalid: false,
-        delegatedTo: null,
-        delegatedToId: null,
-        delegationCredentialId: null,
-        team: {
-          name: "Global",
-        },
-      };
-      logger.debug(
-        `${appMeta.type} is a global app, injecting credential`,
-        safeStringify(getPiiFreeCredential(credential))
-      );
-      appCredentials.push(credential);
-    }
+    /** If the app is a globally installed one, inject its key once */
+    if (
+      appMeta.isGlobal &&
+      !appCredentials.some((c) => c.userId === 0 && c.appId === appMeta.slug)
+    ) {
+      if (!appMeta.key) {
+        logger.warn(
+          `${appMeta.type} marked as global but has no key; skipping credential injection for ${appMeta.slug}`
+        );
+      } else {
+        const credential = {
+          id: 0,
+          type: appMeta.type,
+          key: appMeta.key,
+          userId: 0,
+          user: { email: "" },
+          teamId: null,
+          appId: appMeta.slug,
+          invalid: false,
+          delegatedTo: null,
+          delegatedToId: null,
+          delegationCredentialId: null,
+          team: {
+            name: "Global",
+          },
+        };
+        logger.debug(
+          `${appMeta.type} is a global app, injecting credential`,
+          safeStringify(getPiiFreeCredential(credential))
+        );
+        appCredentials.push(credential);
+      }
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (appMeta.isGlobal) {
const credential = {
id: 0,
type: appMeta.type,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
key: appMeta.key!,
userId: 0,
user: { email: "" },
teamId: null,
appId: appMeta.slug,
invalid: false,
delegatedTo: null,
delegatedToId: null,
delegationCredentialId: null,
team: {
name: "Global",
},
};
logger.debug(
`${appMeta.type} is a global app, injecting credential`,
safeStringify(getPiiFreeCredential(credential))
);
appCredentials.push(credential);
}
/** If the app is a globally installed one, inject its key once */
if (
appMeta.isGlobal &&
!appCredentials.some((c) => c.userId === 0 && c.appId === appMeta.slug)
) {
if (!appMeta.key) {
logger.warn(
`${appMeta.type} marked as global but has no key; skipping credential injection for ${appMeta.slug}`
);
} else {
const credential = {
id: 0,
type: appMeta.type,
key: appMeta.key,
userId: 0,
user: { email: "" },
teamId: null,
appId: appMeta.slug,
invalid: false,
delegatedTo: null,
delegatedToId: null,
delegationCredentialId: null,
team: {
name: "Global",
},
};
logger.debug(
`${appMeta.type} is a global app, injecting credential`,
safeStringify(getPiiFreeCredential(credential))
);
appCredentials.push(credential);
}
}

Comment on lines 69 to 84
const credential: (typeof appCredentials)[number] | null = appCredentials[0] || null;

reducedArray.push({
...appMeta,
/**
* @deprecated use `credentials`
*/
credential,
credentials: appCredentials,
/** Option to display in `location` field while editing event types */
locationOption,
});

return reducedArray;
}, [] as (App & { credential: CredentialDataWithTeamName; credentials: CredentialDataWithTeamName[]; locationOption: LocationOption | null })[]);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Type unsoundness: credential can be null but return type forces non-null

credential is computed with a || null fallback, yet the accumulator type requires a non-null CredentialDataWithTeamName. This can mask errors and cause downstream NPEs.

Apply:

-  }, [] as (App & { credential: CredentialDataWithTeamName; credentials: CredentialDataWithTeamName[]; locationOption: LocationOption | null })[]);
+  }, [] as (App & {
+    credential: CredentialDataWithTeamName | null;
+    credentials: CredentialDataWithTeamName[];
+    locationOption: LocationOption | null;
+  })[]);

Optionally, make the function’s return type explicit so callers get correct typing:

-export const prepareAppsWithCredentials = (
+export const prepareAppsWithCredentials: (
+  apps: App[],
+  credentials: CredentialDataWithTeamName[],
+  filterOnCredentials?: boolean
+) => Array<App & {
+  credential: CredentialDataWithTeamName | null;
+  credentials: CredentialDataWithTeamName[];
+  locationOption: LocationOption | null;
+}> = (
   apps: App[],
   credentials: CredentialDataWithTeamName[],
   filterOnCredentials?: boolean
 ) => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const credential: (typeof appCredentials)[number] | null = appCredentials[0] || null;
reducedArray.push({
...appMeta,
/**
* @deprecated use `credentials`
*/
credential,
credentials: appCredentials,
/** Option to display in `location` field while editing event types */
locationOption,
});
return reducedArray;
}, [] as (App & { credential: CredentialDataWithTeamName; credentials: CredentialDataWithTeamName[]; locationOption: LocationOption | null })[]);
};
const credential: (typeof appCredentials)[number] | null = appCredentials[0] || null;
reducedArray.push({
...appMeta,
/**
* @deprecated use `credentials`
*/
credential,
credentials: appCredentials,
/** Option to display in `location` field while editing event types */
locationOption,
});
return reducedArray;
}, [] as (App & {
credential: CredentialDataWithTeamName | null;
credentials: CredentialDataWithTeamName[];
locationOption: LocationOption | null;
})[]);
};
🤖 Prompt for AI Agents
In packages/app-store/_utils/prepareAppsWithCredentials.ts around lines 69–84,
the code sets credential = appCredentials[0] || null but the accumulator's
return type forces credential to be non-null; update the accumulator return type
to allow credential to be CredentialDataWithTeamName | null (and keep
credentials: CredentialDataWithTeamName[]), or alternatively ensure credential
is always non-null (e.g., filter out entries with empty appCredentials before
reduce or provide a safe default), then make the function's return type explicit
to reflect this change so callers receive correct typing and avoid masked NPEs.

Comment on lines +8 to +10
export const getVideoApps = (credentials: CredentialDataWithTeamName[], filterOnCredentials?: boolean) => {
return prepareAppsWithCredentials(ALL_VIDEO_APPS, credentials, filterOnCredentials);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Redact credential.key before returning video apps

Per our guideline, never return credential.key to clients. Sanitize the credentials array and make the single credential nullable.

Apply:

-export const getVideoApps = (credentials: CredentialDataWithTeamName[], filterOnCredentials?: boolean) => {
-  return prepareAppsWithCredentials(ALL_VIDEO_APPS, credentials, filterOnCredentials);
-};
+export const getVideoApps = (
+  credentials: CredentialDataWithTeamName[],
+  filterOnCredentials?: boolean
+) => {
+  const apps = prepareAppsWithCredentials(ALL_VIDEO_APPS, credentials, filterOnCredentials);
+  return apps.map(({ credentials: creds, ...app }) => {
+    const sanitizedCredentials: Omit<CredentialDataWithTeamName, "key">[] = creds.map(
+      ({ key, ...rest }) => rest
+    );
+    return {
+      ...app,
+      credentials: sanitizedCredentials,
+      credential: sanitizedCredentials[0] ?? null,
+    };
+  });
+};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const getVideoApps = (credentials: CredentialDataWithTeamName[], filterOnCredentials?: boolean) => {
return prepareAppsWithCredentials(ALL_VIDEO_APPS, credentials, filterOnCredentials);
};
export const getVideoApps = (
credentials: CredentialDataWithTeamName[],
filterOnCredentials?: boolean
) => {
const apps = prepareAppsWithCredentials(ALL_VIDEO_APPS, credentials, filterOnCredentials);
return apps.map(({ credentials: creds, ...app }) => {
const sanitizedCredentials: Omit<CredentialDataWithTeamName, "key">[] = creds.map(
({ key, ...rest }) => rest
);
return {
...app,
credentials: sanitizedCredentials,
credential: sanitizedCredentials[0] ?? null,
};
});
};
🤖 Prompt for AI Agents
In packages/app-store/_utils/videos/getVideoApps.ts around lines 8 to 10, the
function currently forwards credentials (including credential.key) to clients;
sanitize the credentials before returning: map the credentials array to remove
or null out the key field on each CredentialDataWithTeamName (e.g., copy all
fields except set key = null or delete key) and update the signature/usage so
any single credential passed into prepareAppsWithCredentials is nullable; ensure
prepareAppsWithCredentials is called with the sanitized array (and the
possibly-null single credential) so no credential.key values are returned to
clients.


return reducedArray;
}, [] as (App & { credential: CredentialDataWithTeamName; credentials: CredentialDataWithTeamName[]; locationOption: LocationOption | null })[]);
const apps = prepareAppsWithCredentials(ALL_APPS, credentials, filterOnCredentials);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Redact credential.key before returning from getApps

This function often feeds UI via tRPC. Sanitize secrets prior to returning to avoid leaking key.

Apply:

-function getApps(credentials: CredentialDataWithTeamName[], filterOnCredentials?: boolean) {
-  const apps = prepareAppsWithCredentials(ALL_APPS, credentials, filterOnCredentials);
-
-  return apps;
-}
+function getApps(credentials: CredentialDataWithTeamName[], filterOnCredentials?: boolean) {
+  const prepared = prepareAppsWithCredentials(ALL_APPS, credentials, filterOnCredentials);
+  const apps = prepared.map(({ credentials: creds, ...app }) => {
+    const sanitizedCredentials: Omit<CredentialDataWithTeamName, "key">[] = creds.map(
+      ({ key, ...rest }) => rest
+    );
+    return {
+      ...app,
+      credentials: sanitizedCredentials,
+      credential: sanitizedCredentials[0] ?? null,
+    };
+  });
+  return apps;
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const apps = prepareAppsWithCredentials(ALL_APPS, credentials, filterOnCredentials);
function getApps(credentials: CredentialDataWithTeamName[], filterOnCredentials?: boolean) {
const prepared = prepareAppsWithCredentials(ALL_APPS, credentials, filterOnCredentials);
const apps = prepared.map(({ credentials: creds, ...app }) => {
const sanitizedCredentials: Omit<CredentialDataWithTeamName, "key">[] = creds.map(
({ key, ...rest }) => rest
);
return {
...app,
credentials: sanitizedCredentials,
credential: sanitizedCredentials[0] ?? null,
};
});
return apps;
}
🤖 Prompt for AI Agents
In packages/app-store/utils.ts around line 49,
prepareAppsWithCredentials(ALL_APPS, credentials, filterOnCredentials) returns
app objects that include credential.key which must be sanitized before exposing
to the UI; modify the code after that call to produce a shallow/deep copy of the
apps and remove or replace credential.key (e.g., set to undefined or a fixed
"[REDACTED]" string) for each credential entry so secrets are not returned,
ensuring you do not mutate the original credentials array (use map or
structuredClone) and preserve all other fields and types.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (8)
tests/libs/__mocks__/mockDeepHelper.ts (1)

1-10: Make return type explicit and tighten mock typing for better DX

Annotating the helper’s return type helps downstream usage (e.g., mockReset) with full IntelliSense and safer refactors.

Apply this diff:

-import { mockDeep } from "vitest-mock-extended";
+import { mockDeep, type DeepMockProxy } from "vitest-mock-extended";

-export const mockDeepHelper = <T>(mockFnName = "") =>
+export const mockDeepHelper = <T>(mockFnName = ""): DeepMockProxy<T> =>
   mockDeep<T>({
     fallbackMockImplementation: () => {
       throw new Error(
         `Unimplemented ${mockFnName}. You seem to have not mocked the app that you are trying to use`
       );
     },
   });
tests/libs/__mocks__/videoLoadersMock.ts (1)

8-16: Harden ESM default interop in vi.mock factory and provide a named export

Returning the mock object directly can stumble on ESM/CJS default interop. Declare __esModule: true and return an object with a default export. Also export a named symbol for consistency with our guidelines on named exports.

Apply this diff:

-const videoLoadersMock = mockDeepHelper<typeof videoLoaders>("videoLoadersMock");
+const videoLoadersMock = mockDeepHelper<typeof videoLoaders>("videoLoadersMock");

-vi.mock("@calcom/app-store/_utils/videos/videoLoaders", () => videoLoadersMock);
+vi.mock("@calcom/app-store/_utils/videos/videoLoaders", () => ({
+  __esModule: true,
+  default: videoLoadersMock.default,
+}));

 export default videoLoadersMock;
+export { videoLoadersMock };
tests/libs/__mocks__/paymentLoadersMock.ts (1)

8-16: Mirror the ESM mock shape and add a named export

Same rationale as video loaders: ensure __esModule: true and expose a named export for consistency.

-const paymentLoadersMock = mockDeepHelper<typeof paymentLoaders>("paymentLoadersMock");
+const paymentLoadersMock = mockDeepHelper<typeof paymentLoaders>("paymentLoadersMock");

-vi.mock("@calcom/app-store/_utils/payments/paymentLoaders", () => paymentLoadersMock);
+vi.mock("@calcom/app-store/_utils/payments/paymentLoaders", () => ({
+  __esModule: true,
+  default: paymentLoadersMock.default,
+}));

 export default paymentLoadersMock;
+export { paymentLoadersMock };
tests/libs/__mocks__/calendarLoadersMock.ts (1)

8-16: Align calendar loaders mock with robust ESM interop and add named export

Keep the mock shape explicit and export a named symbol to ease future refactors.

-const calendarLoadersMock = mockDeepHelper<typeof calendarLoaders>("calendarLoadersMock");
+const calendarLoadersMock = mockDeepHelper<typeof calendarLoaders>("calendarLoadersMock");

-vi.mock("@calcom/app-store/_utils/calendars/calendarLoaders", () => calendarLoadersMock);
+vi.mock("@calcom/app-store/_utils/calendars/calendarLoaders", () => ({
+  __esModule: true,
+  default: calendarLoadersMock.default,
+}));

 export default calendarLoadersMock;
+export { calendarLoadersMock };
apps/web/test/utils/bookingScenario/bookingScenario.ts (4)

1767-1775: Make failure modes explicit and fix logging label

Currently, if calendarAppsMetaData[key] or the loader mock is missing, the code silently skips mocking and tests may fail deeper in the stack. Fail fast with actionable messages. Also, the log label still says “calendarAppsMock”.

-  log.silly(`Mocking ${appStoreLookupKey} on calendarAppsMock`);
+  log.silly(`Mocking ${appStoreLookupKey} on calendarLoadersMock`);

-  const app = calendarAppsMetaData[metadataLookupKey as keyof typeof calendarAppsMetaData];
+  const app = calendarAppsMetaData[metadataLookupKey as keyof typeof calendarAppsMetaData];
+  if (!app) {
+    throw new Error(
+      `calendarAppsMetaData["${String(
+        metadataLookupKey
+      )}"] not found. Ensure the calendar metadata is generated/available for this key.`
+    );
+  }
 
-  const appMock = calendarLoadersMock.default[appStoreLookupKey as keyof typeof calendarLoadersMock.default];
+  const appMock =
+    calendarLoadersMock.default[appStoreLookupKey as keyof typeof calendarLoadersMock.default];
+  if (!appMock) {
+    throw new Error(
+      `calendarLoadersMock.default["${String(
+        appStoreLookupKey
+      )}"] is missing. Did you import tests/libs/__mocks__/calendarLoadersMock and set up the loader key?`
+    );
+  }

1959-2020: Guard missing video loader keys and simplify the mock wiring

Add an explicit check so tests fail fast if the loader key isn’t present. Also, slightly simplify the promise creation.

-  videoLoadersMock.default[appStoreLookupKey as keyof typeof videoLoadersMock.default].mockImplementation(
-    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-    // @ts-expect-error FIXME
-    () => {
-      return new Promise((resolve) => {
-        resolve({
-          lib: {
-            // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-            // @ts-expect-error FIXME
-            VideoApiAdapter: (credential) => {
+  const videoLoader =
+    videoLoadersMock.default[appStoreLookupKey as keyof typeof videoLoadersMock.default];
+  if (!videoLoader) {
+    throw new Error(
+      `videoLoadersMock.default["${String(
+        appStoreLookupKey
+      )}"] is missing. Ensure the video loader key is mocked before use.`
+    );
+  }
+  videoLoader.mockImplementation(
+    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+    // @ts-expect-error FIXME
+    async () => {
+      return {
+        lib: {
+          // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+          // @ts-expect-error FIXME
+          VideoApiAdapter: (credential) => {
               return {
                 // eslint-disable-next-line @typescript-eslint/no-explicit-any
                 createMeeting: (...rest: any[]) => {
                   if (creationCrash) {
                     throw new Error("MockVideoApiAdapter.createMeeting fake error");
                   }
                   createMeetingCalls.push({
                     credential,
                     args: rest,
                   });
 
                   return Promise.resolve({
                     type: appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata].type,
                     ...videoMeetingData,
                   });
                 },
                 // eslint-disable-next-line @typescript-eslint/no-explicit-any
                 updateMeeting: async (...rest: any[]) => {
                   if (updationCrash) {
                     throw new Error("MockVideoApiAdapter.updateMeeting fake error");
                   }
                   const [bookingRef, calEvent] = rest;
                   updateMeetingCalls.push({
                     credential,
                     args: rest,
                   });
                   if (!bookingRef.type) {
                     throw new Error("bookingRef.type is not defined");
                   }
                   if (!calEvent.organizer) {
                     throw new Error("calEvent.organizer is not defined");
                   }
                   log.silly("MockVideoApiAdapter.updateMeeting", JSON.stringify({ bookingRef, calEvent }));
                   return Promise.resolve({
                     type: appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata].type,
                     ...videoMeetingData,
                   });
                 },
                 // eslint-disable-next-line @typescript-eslint/no-explicit-any
                 deleteMeeting: async (...rest: any[]) => {
                   log.silly("MockVideoApiAdapter.deleteMeeting", JSON.stringify(rest));
                   deleteMeetingCalls.push({
                     credential,
                     args: rest,
                   });
                 },
               };
-            },
-          },
-        });
-      });
-    }
+            },
+          },
+        };
+    }
   );

2071-2085: Add a guard for missing payment loader keys and simplify promise style

Failing early makes test failures clearer; using async cleans up the promise ceremony.

-  paymentLoadersMock.default[appStoreLookupKey as keyof typeof paymentLoadersMock.default].mockImplementation(
-    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-    // @ts-expect-error FIXME
-    () => {
-      return new Promise((resolve) => {
-        resolve({
-          lib: {
-            // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-            // @ts-expect-error FIXME
-            PaymentService: MockPaymentService,
-          },
-        });
-      });
-    }
-  );
+  const paymentLoader =
+    paymentLoadersMock.default[appStoreLookupKey as keyof typeof paymentLoadersMock.default];
+  if (!paymentLoader) {
+    throw new Error(
+      `paymentLoadersMock.default["${String(
+        appStoreLookupKey
+      )}"] is missing. Ensure the payment loader key is mocked before use.`
+    );
+  }
+  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+  // @ts-expect-error FIXME
+  paymentLoader.mockImplementation(async () => {
+    return {
+      lib: {
+        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+        // @ts-expect-error FIXME
+        PaymentService: MockPaymentService,
+      },
+    };
+  });

2103-2115: Prefer ts-expect-error over ts-ignore for intentional type breaks

@ts-expect-error ensures we notice if the error stops being necessary in future refactors.

-  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-  //@ts-ignore
+  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+  // @ts-expect-error - dynamic key on deep mock
   videoLoadersMock.default[appStoreLookupKey].mockImplementation(() => {
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between df97c69 and b53f30f.

📒 Files selected for processing (6)
  • apps/web/test/utils/bookingScenario/bookingScenario.ts (9 hunks)
  • tests/libs/__mocks__/app-store.ts (1 hunks)
  • tests/libs/__mocks__/calendarLoadersMock.ts (1 hunks)
  • tests/libs/__mocks__/mockDeepHelper.ts (1 hunks)
  • tests/libs/__mocks__/paymentLoadersMock.ts (1 hunks)
  • tests/libs/__mocks__/videoLoadersMock.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/libs/mocks/app-store.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

**/*.ts: For Prisma queries, only select data you need; never use include, always use select
Ensure the credential.key field is never returned from tRPC endpoints or APIs

Files:

  • tests/libs/__mocks__/videoLoadersMock.ts
  • tests/libs/__mocks__/mockDeepHelper.ts
  • tests/libs/__mocks__/calendarLoadersMock.ts
  • tests/libs/__mocks__/paymentLoadersMock.ts
  • apps/web/test/utils/bookingScenario/bookingScenario.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js .utc() in hot paths like loops

Files:

  • tests/libs/__mocks__/videoLoadersMock.ts
  • tests/libs/__mocks__/mockDeepHelper.ts
  • tests/libs/__mocks__/calendarLoadersMock.ts
  • tests/libs/__mocks__/paymentLoadersMock.ts
  • apps/web/test/utils/bookingScenario/bookingScenario.ts
**/*.{ts,tsx,js,jsx}

⚙️ CodeRabbit configuration file

Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.

Files:

  • tests/libs/__mocks__/videoLoadersMock.ts
  • tests/libs/__mocks__/mockDeepHelper.ts
  • tests/libs/__mocks__/calendarLoadersMock.ts
  • tests/libs/__mocks__/paymentLoadersMock.ts
  • apps/web/test/utils/bookingScenario/bookingScenario.ts
🧬 Code graph analysis (4)
tests/libs/__mocks__/videoLoadersMock.ts (1)
tests/libs/__mocks__/mockDeepHelper.ts (1)
  • mockDeepHelper (3-10)
tests/libs/__mocks__/calendarLoadersMock.ts (1)
tests/libs/__mocks__/mockDeepHelper.ts (1)
  • mockDeepHelper (3-10)
tests/libs/__mocks__/paymentLoadersMock.ts (1)
tests/libs/__mocks__/mockDeepHelper.ts (1)
  • mockDeepHelper (3-10)
apps/web/test/utils/bookingScenario/bookingScenario.ts (1)
packages/app-store/_utils/calendars/calendarAppsMetaData.ts (1)
  • calendarAppsMetaData (11-11)
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (5)
apps/web/test/utils/bookingScenario/bookingScenario.ts (5)

2-6: Switching to loader-specific mocks looks good

Moving from the monolithic appStoreMock to targeted calendarLoadersMock, videoLoadersMock, and paymentLoadersMock improves isolation and speeds up test setup. No issues here.


1723-1723: Type tightening on mockCalendar’s key is a good change

Using keyof typeof calendarAppsMetaData ensures compile-time alignment between metadata and mock usage.


1906-1917: Helper retype to calendar metadata keys looks good

The API remains the same but is now safer against mismatched keys. No changes requested.


1920-1926: Crash helpers read well and mirror the success path

Clear, minimal helpers to force failure paths in tests. Good to keep them symmetric with mockCalendar.


17-17: calendarAppsMetaData contains the expected calendar keys

I’ve verified that the auto-generated metadata includes all the required calendar entries, so calendarAppsMetaData will be populated correctly and won’t cause mockCalendar to crash:

• In packages/app-store/calendarApps.metadata.generated.ts, the exported calendarAppsMetadata object contains
– applecalendar
– googlecalendar
– office365calendar
(alongside all other calendar keys)
as shown by the generated file’s mapping.
• The runtime loader (calendarAppsMetaData.ts) simply normalizes and re-exports each of these entries, so tests importing calendarAppsMetaData will have all needed keys.

No changes are necessary here.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
packages/lib/setupPaymentService.ts (1)

1-16: Undefined identifiers (app, credential), unsafe optional chaining, and ts-expect-error — replace with typed, guarded loader flow

The current implementation won’t compile/run: app and credential are not defined in scope; ts-expect-error hides a typing issue; optional chaining on the call site is unnecessary; isSetupAlready likely needs await. This also mirrors a previously raised concern about error suppression. Fix by accepting explicit inputs, guarding the loader/module shape, and removing console logging.

Apply this diff:

-import paymentLoaders, { type PaymentLoaderKey } from "@calcom/app-store/_utils/payments/paymentLoaders";
-import type { PaymentApp } from "@calcom/types/PaymentService";
-
-export const setupPaymentService = async () => {
-  console.log("Checking payment setup for app", app.name);
-  // @ts-expect-error FIXME
-  const paymentApp = (await paymentLoaders[app.dirName as PaymentLoaderKey]?.()) as PaymentApp | null;
-  if (paymentApp && "lib" in paymentApp && paymentApp?.lib && "PaymentService" in paymentApp?.lib) {
-    const PaymentService = paymentApp.lib.PaymentService;
-    const paymentInstance = new PaymentService(credential);
-
-    const isPaymentSetupAlready = paymentInstance.isSetupAlready();
-
-    return isPaymentSetupAlready;
-  }
-};
+import paymentLoaders, { type PaymentLoaderKey } from "@calcom/app-store/_utils/payments/paymentLoaders";
+
+type PaymentServiceCtor = new (credential: unknown) => {
+  isSetupAlready?: () => unknown | Promise<unknown>;
+};
+
+function hasPaymentService(mod: unknown): mod is { lib: { PaymentService: PaymentServiceCtor } } {
+  return (
+    !!mod &&
+    typeof mod === "object" &&
+    "lib" in (mod as any) &&
+    (mod as any).lib &&
+    typeof (mod as any).lib === "object" &&
+    "PaymentService" in (mod as any).lib &&
+    typeof (mod as any).lib.PaymentService === "function"
+  );
+}
+
+export const setupPaymentService = async ({
+  dirName,
+  credential,
+}: {
+  dirName: string;
+  credential: unknown;
+}): Promise<boolean | undefined> => {
+  const key = dirName as PaymentLoaderKey;
+  const loader = paymentLoaders[key];
+  if (!loader) return undefined;
+
+  const mod = await loader();
+  if (!hasPaymentService(mod)) return undefined;
+
+  try {
+    const PaymentService = mod.lib.PaymentService;
+    const instance = new PaymentService(credential);
+    const result = instance.isSetupAlready?.();
+    const resolved = await Promise.resolve(result);
+    return typeof resolved === "boolean" ? resolved : undefined;
+  } catch {
+    // Swallow and return undefined; caller treats undefined as "not required/unknown"
+    return undefined;
+  }
+};
  • Removes console.log to avoid noisy logs on hot paths.
  • Eliminates ts-expect-error by narrowing via a type guard.
  • Replaces optional chaining call with an explicit loader existence check (fixes Biome noUnsafeOptionalChaining).
  • Awaits isSetupAlready() and normalizes sync/async returns.

If you want, I can add unit tests covering:

  • no loader found → undefined
  • loader without PaymentService → undefined
  • PaymentService.isSetupAlready sync/async → true/false
  • constructor or method throws → undefined
#!/bin/bash
# Sanity-check Biome warnings on this file after the refactor
rg -n "ts-expect-error|\\?\\." packages/lib/setupPaymentService.ts -n -C0
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b53f30f and 02da408.

📒 Files selected for processing (2)
  • packages/lib/getConnectedApps.ts (2 hunks)
  • packages/lib/setupPaymentService.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*Service.ts

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Service files must include Service suffix, use PascalCase matching exported class, and avoid generic names (e.g., MembershipService.ts)

Files:

  • packages/lib/setupPaymentService.ts
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

**/*.ts: For Prisma queries, only select data you need; never use include, always use select
Ensure the credential.key field is never returned from tRPC endpoints or APIs

Files:

  • packages/lib/setupPaymentService.ts
  • packages/lib/getConnectedApps.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js .utc() in hot paths like loops

Files:

  • packages/lib/setupPaymentService.ts
  • packages/lib/getConnectedApps.ts
**/*.{ts,tsx,js,jsx}

⚙️ CodeRabbit configuration file

Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.

Files:

  • packages/lib/setupPaymentService.ts
  • packages/lib/getConnectedApps.ts
🧬 Code graph analysis (2)
packages/lib/setupPaymentService.ts (1)
packages/app-store/_utils/payments/paymentLoaders.ts (1)
  • PaymentLoaderKey (16-16)
packages/lib/getConnectedApps.ts (1)
packages/lib/setupPaymentService.ts (1)
  • setupPaymentService (4-16)
🪛 Biome (2.1.2)
packages/lib/setupPaymentService.ts

[error] 8-8: Unsafe usage of optional chaining.

If it short-circuits with 'undefined' the evaluation will throw TypeError here:

(lint/correctness/noUnsafeOptionalChaining)

⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Install dependencies / Yarn install & cache
🔇 Additional comments (1)
packages/lib/getConnectedApps.ts (1)

16-16: Named import looks good

Using a named export for setupPaymentService aligns with our tree-shaking guideline and avoids default exports.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
packages/lib/setupPaymentService.ts (1)

16-18: Remove @ts-expect-error and safely resolve the loader

Avoid suppressing type errors. Narrow the loader first and only call it if present. This removes the suppression and makes the control flow explicit.

Apply this diff:

-  // @ts-expect-error FIXME
-  const paymentApp = (await paymentLoaders[app.dirName as PaymentLoaderKey]?.()) as PaymentApp | null;
+  const loader = paymentLoaders[app.dirName as PaymentLoaderKey];
+  const paymentApp = loader ? ((await loader()) as PaymentApp | null) : null;
🧹 Nitpick comments (6)
packages/lib/setupPaymentService.ts (5)

9-15: Add explicit return type to the exported API

Make the return type explicit to prevent accidental widening in future edits and to improve DX.

-export const setupPaymentService = async ({
+export const setupPaymentService = async ({
   app,
   credential,
 }: {
   app: Omit<EnabledApp, "credentials" | "key" | "credential">;
   credential: CredentialDataWithTeamName;
-}) => {
+}): Promise<boolean | undefined> => {

1-1: Prefer named exports over default exports for better tree-shaking

If feasible, refactor @calcom/app-store/_utils/payments/paymentLoaders to export paymentLoaders as a named export and import it accordingly. This aligns with our guideline to avoid default exports for shared modules.

Example:

// In paymentLoaders.ts
export const paymentLoaders = { /* ... */ };

// Here
import { paymentLoaders } from "@calcom/app-store/_utils/payments/paymentLoaders";

7-8: Optional: Introduce a small type guard to encapsulate shape checks

Encapsulating the runtime checks into a reusable guard will reduce repetition and silence lints without resorting to any.

Proposed addition (place near the top of the file):

+type PaymentServiceCtor = new (c: CredentialDataWithTeamName) => {
+  isSetupAlready(): boolean | Promise<boolean>;
+};
+
+function hasPaymentService(x: unknown): x is { lib: { PaymentService: PaymentServiceCtor } } {
+  if (!x || typeof x !== "object") return false;
+  const lib = (x as any).lib;
+  return lib && typeof lib === "object" && typeof lib.PaymentService === "function";
+}

Then the main check becomes:

if (paymentApp && hasPaymentService(paymentApp)) { /* ... */ }

I can wire this in for you if you prefer.


22-24: Consistency: await the service call

Even if isSetupAlready() can be sync, awaiting it makes the return consistently Promise<boolean | undefined> and prevents leaking a rejected promise upwards.

-    const isPaymentSetupAlready = paymentInstance.isSetupAlready();
+    const isPaymentSetupAlready = await paymentInstance.isSetupAlready();

9-26: Add tests for loader missing, invalid module shape, and happy-path

This module is a new seam. Please add unit tests that cover:

  • No loader for app.dirName → returns undefined
  • Loader resolves to object without lib.PaymentService → returns undefined
  • Happy path: PaymentService#isSetupAlready() returns boolean and promise variants

I can generate Jest/Vitest tests using your existing tests/libs/__mocks__/paymentLoadersMock.ts. Want me to draft them?

packages/lib/getConnectedApps.ts (1)

183-187: Guard setupPaymentService against failures in getConnectedApps.ts

The only invocation of setupPaymentService is in packages/lib/getConnectedApps.ts at line 186. Wrapping this call in a try/catch ensures that if the dynamic import or service initialization fails, it degrades gracefully without preventing other apps from rendering.

• File: packages/lib/getConnectedApps.ts
Line: 186

Suggested diff:

       let isSetupAlready = undefined;
       if (credential && app.categories.includes("payment")) {
-        isSetupAlready = await setupPaymentService({ app, credential });
+        try {
+          isSetupAlready = await setupPaymentService({ app, credential });
+        } catch (err) {
+          // Optionally add debug logging here
+          isSetupAlready = undefined;
+        }
       }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 02da408 and ffb0fcb.

📒 Files selected for processing (2)
  • packages/lib/getConnectedApps.ts (2 hunks)
  • packages/lib/setupPaymentService.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

**/*.ts: For Prisma queries, only select data you need; never use include, always use select
Ensure the credential.key field is never returned from tRPC endpoints or APIs

Files:

  • packages/lib/getConnectedApps.ts
  • packages/lib/setupPaymentService.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js .utc() in hot paths like loops

Files:

  • packages/lib/getConnectedApps.ts
  • packages/lib/setupPaymentService.ts
**/*.{ts,tsx,js,jsx}

⚙️ CodeRabbit configuration file

Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.

Files:

  • packages/lib/getConnectedApps.ts
  • packages/lib/setupPaymentService.ts
**/*Service.ts

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Service files must include Service suffix, use PascalCase matching exported class, and avoid generic names (e.g., MembershipService.ts)

Files:

  • packages/lib/setupPaymentService.ts
🧬 Code graph analysis (1)
packages/lib/setupPaymentService.ts (1)
packages/app-store/_utils/payments/paymentLoaders.ts (1)
  • PaymentLoaderKey (16-16)
🪛 Biome (2.1.2)
packages/lib/setupPaymentService.ts

[error] 18-18: Unsafe usage of optional chaining.

If it short-circuits with 'undefined' the evaluation will throw TypeError here:

(lint/correctness/noUnsafeOptionalChaining)

⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Install dependencies / Yarn install & cache
  • GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (2)
packages/lib/getConnectedApps.ts (2)

16-16: LGTM: Scoped import of setupPaymentService

Importing only the new helper keeps this module lean and aligns with the refactor toward loaders.


149-156: PII check: credential.key not leaked

Sanitization by omitting credentials, credential, and key from app before returning is preserved. Good.

Comment on lines +18 to +25
if (paymentApp && "lib" in paymentApp && paymentApp?.lib && "PaymentService" in paymentApp?.lib) {
const PaymentService = paymentApp.lib.PaymentService;
const paymentInstance = new PaymentService(credential);

const isPaymentSetupAlready = paymentInstance.isSetupAlready();

return isPaymentSetupAlready;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix unsafe optional chaining with in operator and strengthen runtime guards

Biome is right: using optional chaining with in is unsafe. Also, check that PaymentService is a constructor and await isSetupAlready() to normalize return type and avoid unhandled rejections.

Apply this diff:

-  if (paymentApp && "lib" in paymentApp && paymentApp?.lib && "PaymentService" in paymentApp?.lib) {
-    const PaymentService = paymentApp.lib.PaymentService;
-    const paymentInstance = new PaymentService(credential);
-
-    const isPaymentSetupAlready = paymentInstance.isSetupAlready();
-
-    return isPaymentSetupAlready;
-  }
+  if (
+    paymentApp &&
+    typeof paymentApp === "object" &&
+    "lib" in paymentApp &&
+    paymentApp.lib &&
+    typeof (paymentApp as any).lib === "object" &&
+    "PaymentService" in (paymentApp as any).lib &&
+    typeof (paymentApp as any).lib.PaymentService === "function"
+  ) {
+    const PaymentService = (paymentApp as any).lib.PaymentService as new (
+      credential: CredentialDataWithTeamName
+    ) => { isSetupAlready(): boolean | Promise<boolean> };
+    const paymentInstance = new PaymentService(credential);
+    const isPaymentSetupAlready = await paymentInstance.isSetupAlready();
+    return isPaymentSetupAlready;
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (paymentApp && "lib" in paymentApp && paymentApp?.lib && "PaymentService" in paymentApp?.lib) {
const PaymentService = paymentApp.lib.PaymentService;
const paymentInstance = new PaymentService(credential);
const isPaymentSetupAlready = paymentInstance.isSetupAlready();
return isPaymentSetupAlready;
}
if (
paymentApp &&
typeof paymentApp === "object" &&
"lib" in paymentApp &&
paymentApp.lib &&
typeof (paymentApp as any).lib === "object" &&
"PaymentService" in (paymentApp as any).lib &&
typeof (paymentApp as any).lib.PaymentService === "function"
) {
const PaymentService = (paymentApp as any).lib.PaymentService as new (
credential: CredentialDataWithTeamName
) => { isSetupAlready(): boolean | Promise<boolean> };
const paymentInstance = new PaymentService(credential);
const isPaymentSetupAlready = await paymentInstance.isSetupAlready();
return isPaymentSetupAlready;
}
🧰 Tools
🪛 Biome (2.1.2)

[error] 18-18: Unsafe usage of optional chaining.

If it short-circuits with 'undefined' the evaluation will throw TypeError here:

(lint/correctness/noUnsafeOptionalChaining)

🤖 Prompt for AI Agents
In packages/lib/setupPaymentService.ts around lines 18–25, avoid using optional
chaining with the `in` operator and add stronger runtime guards: explicitly
check that paymentApp is an object, that it has a `lib` property which is an
object, that `PaymentService` exists on paymentApp.lib and is a constructor
(typeof === "function"); instantiate with `new PaymentService(credential)` only
after those checks, verify that the resulting instance has an `isSetupAlready`
method (typeof === "function"), `await` the call to `isSetupAlready()` to
normalize async behavior and avoid unhandled rejections, coerce the result to a
boolean before returning, and return a safe default (e.g., false) if any guard
fails.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (3)
packages/lib/setupPaymentService.ts (1)

14-24: Fix unsafe optional chaining with in, remove ts-expect-error, add runtime guards, and await the result

Biome is correct: using optional chaining on the RHS of in is unsafe and can throw. Also, returning the raw promise from isSetupAlready() defers errors and complicates consumers. Guard shapes, await, and normalize to boolean.

-  // @ts-expect-error FIXME
-  const paymentApp = (await paymentLoaders[app.dirName as PaymentLoaderKey]?.()) as PaymentApp | null;
-  if (paymentApp && "lib" in paymentApp && paymentApp?.lib && "PaymentService" in paymentApp?.lib) {
-    const PaymentService = paymentApp.lib.PaymentService;
-    const paymentInstance = new PaymentService(credential);
-
-    const isPaymentSetupAlready = paymentInstance.isSetupAlready();
-
-    return isPaymentSetupAlready;
-  }
+  const loadPaymentApp = paymentLoaders[app.dirName as PaymentLoaderKey];
+  if (typeof loadPaymentApp !== "function") return undefined;
+
+  let paymentApp: unknown;
+  try {
+    paymentApp = await loadPaymentApp();
+  } catch {
+    // Treat loader failures as "unknown"
+    return undefined;
+  }
+
+  if (!paymentApp || typeof paymentApp !== "object") return undefined;
+  const lib = (paymentApp as any).lib;
+  if (!lib || typeof lib !== "object") return undefined;
+
+  const PaymentService = (lib as any).PaymentService as
+    | (new (credential: CredentialDataWithTeamName) => { isSetupAlready(): boolean | Promise<boolean> })
+    | undefined;
+  if (typeof PaymentService !== "function") return undefined;
+
+  const paymentInstance = new PaymentService(credential);
+  if (typeof (paymentInstance as any)?.isSetupAlready !== "function") return undefined;
+
+  const isPaymentSetupAlready = await paymentInstance.isSetupAlready();
+  return Boolean(isPaymentSetupAlready);
packages/app-store/_utils/prepareAppsWithCredentials.ts (2)

75-86: Redact secret credential.key before returning to clients

Both credential and credentials are returned with raw key values. This violates the guideline to never expose credential.key via tRPC/API and is a high‑risk secret leak for the /apps/installed/* pages. Sanitize before pushing to reducedArray.

Apply this diff within the function to sanitize the outgoing payload:

-    const credential: (typeof appCredentials)[number] | null = appCredentials[0] || null;
-
-    reducedArray.push({
+    // Sanitize credentials before returning them across server boundaries
+    const safeCredentials = appCredentials.map((c) => getPiiFreeCredential(c));
+    const safeCredential = safeCredentials[0] ?? null;
+
+    reducedArray.push({
       ...appMeta,
       /**
        * @deprecated use `credentials`
        */
-      credential,
-      credentials: appCredentials,
+      credential: safeCredential,
+      credentials: safeCredentials,
       /** Option to display in `location` field while editing event types */
       locationOption,
     });

Optionally, if you prefer to drop the key field entirely rather than return a boolean presence flag, replace the map with:

const safeCredentials = appCredentials.map(({ key: _omit, ...rest }) => rest);

Verification script to ensure no raw credential.key crosses API boundaries:

#!/bin/bash
# 1) Find all places that return app-store data via TRPC/HTTP
rg -nP -C3 --type=ts '(createTRPCRouter|publicProcedure|privateProcedure|router\(|NextResponse\.json|res\.json)\b'

# 2) Ensure none of these responses include credential.key
rg -nP -C2 --type=ts '\bcredential\.key\b|credentials?:\s*\[?[^{\]]*key'

# 3) Confirm sanitize happens at all app helpers
rg -nP -C3 --type=ts 'prepareAppsWithCredentials\(|get(Calendar|Video)Apps\('

22-26: Fix type soundness: credential can be null and (optionally) use a sanitized credential type

Runtime can push credential = null, but PreparedApp requires non-null. This masking via as PreparedApp[] can cause downstream NPEs. Also, once you sanitize, the key type no longer matches the source type.

Apply this diff to make types accurate and future‑proof:

-export type PreparedApp = App & {
-  credential: CredentialDataWithTeamName;
-  credentials: CredentialDataWithTeamName[];
-  locationOption: LocationOption | null;
-};
+// Public shape returned to clients; key is sanitized by getPiiFreeCredential (boolean) or omitted.
+type PublicCredential = ReturnType<typeof getPiiFreeCredential> & { team?: { name: string } | null };
+
+export type PreparedApp = App & {
+  credential: PublicCredential | null;
+  credentials: PublicCredential[];
+  locationOption: LocationOption | null;
+};

Additionally, make the function’s return type explicit (see next comment).

🧹 Nitpick comments (6)
packages/lib/setupPaymentService.ts (3)

1-2: Prefer named export over default import for better tree-shaking and refactors

Per repo guidelines, avoid default exports in libraries. If feasible, switch paymentLoaders to a named export in its module and update this import.

-import paymentLoaders from "@calcom/app-store/_utils/payments/paymentLoaders";
+import { paymentLoaders } from "@calcom/app-store/_utils/payments/paymentLoaders";

Note: This requires changing the exporter in paymentLoaders.ts.


7-13: Annotate an explicit return type to document contract

Callers likely rely on “boolean when available, otherwise undefined.” Make that explicit.

 export const setupPaymentService = async ({
   app,
   credential,
 }: {
   app: Omit<PreparedApp, "credentials" | "key" | "credential">;
   credential: CredentialDataWithTeamName;
-}) => {
+}): Promise<boolean | undefined> => {

Please confirm all callers handle undefined distinctly from false.


7-24: Add targeted tests for loader and service paths

Cover both sync and async isSetupAlready() implementations, missing loader, missing/invalid PaymentService, and loader throwing.

Recommended cases:

  • returns true (boolean)
  • returns Promise
  • loader missing for app.dirName -> undefined
  • loader throws -> undefined
  • lib present but PaymentService missing -> undefined

I can draft Jest tests using the existing paymentLoaders mock. Want me to push a test file?

packages/app-store/_utils/prepareAppsWithCredentials.ts (3)

28-33: Add explicit return type and pre-index credentials for better scalability

Specify the return type to catch regressions at compile time. Also, filtering credentials for each app is O(Napps×Ncreds). Pre-index by appId to reduce to O(Napps + Ncreds). This helps the page-load objective when apps/credentials grow.

Apply:

-export const prepareAppsWithCredentials = (
+export const prepareAppsWithCredentials = (
   apps: App[],
   credentials: CredentialDataWithTeamName[],
   filterOnCredentials?: boolean
-) => {
-  return apps.reduce((reducedArray, appMeta) => {
-    const appCredentials = credentials.filter((credential) => credential.appId === appMeta.slug);
+) : PreparedApp[] => {
+  // Group credentials by appId for O(1) lookup per app
+  const credsByAppId = credentials.reduce<Map<string, CredentialDataWithTeamName[]>>((map, c) => {
+    const list = map.get(c.appId);
+    if (list) list.push(c);
+    else map.set(c.appId, [c]);
+    return map;
+  }, new Map());
+
+  return apps.reduce<PreparedApp[]>((reducedArray, appMeta) => {
+    const appCredentials = [...(credsByAppId.get(appMeta.slug) ?? [])];

40-64: Guard missing global key and avoid duplicate injection; grammar nit

  • If appMeta.key is absent, current ! non-null assertion is unsafe.
  • Prevent pushing multiple synthetic credentials when this function is called repeatedly.
  • Minor: “it’s key” → “its key”.

Apply:

-    /** If the app is a globally installed one, let's inject it's key */
-    if (appMeta.isGlobal) {
+    /** If the app is a globally installed one, inject its key once */
+    if (
+      appMeta.isGlobal &&
+      !appCredentials.some((c) => c.userId === 0 && c.appId === appMeta.slug)
+    ) {
+      if (!appMeta.key) {
+        logger.warn(
+          `${appMeta.type} marked as global but has no key; skipping credential injection for ${appMeta.slug}`
+        );
+      } else {
       const credential = {
         id: 0,
         type: appMeta.type,
-        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        key: appMeta.key!,
+        key: appMeta.key,
         userId: 0,
         user: { email: "" },
         teamId: null,
         appId: appMeta.slug,
         invalid: false,
         delegatedTo: null,
         delegatedToId: null,
         delegationCredentialId: null,
         team: {
           name: "Global",
         },
       };
       logger.debug(
         `${appMeta.type} is a global app, injecting credential`,
         safeStringify(getPiiFreeCredential(credential))
       );
       appCredentials.push(credential);
-    }
+      }
+    }

88-90: Remove unsafe cast and rely on accurate types

After adding an explicit return type and updating PreparedApp, this as PreparedApp[] cast becomes unnecessary. Remove it to let the compiler enforce correctness.

Apply:

-  }, [] as PreparedApp[]);
+  }, []);
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ffb0fcb and fb71c07.

📒 Files selected for processing (2)
  • packages/app-store/_utils/prepareAppsWithCredentials.ts (1 hunks)
  • packages/lib/setupPaymentService.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

**/*.ts: For Prisma queries, only select data you need; never use include, always use select
Ensure the credential.key field is never returned from tRPC endpoints or APIs

Files:

  • packages/app-store/_utils/prepareAppsWithCredentials.ts
  • packages/lib/setupPaymentService.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js .utc() in hot paths like loops

Files:

  • packages/app-store/_utils/prepareAppsWithCredentials.ts
  • packages/lib/setupPaymentService.ts
**/*.{ts,tsx,js,jsx}

⚙️ CodeRabbit configuration file

Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.

Files:

  • packages/app-store/_utils/prepareAppsWithCredentials.ts
  • packages/lib/setupPaymentService.ts
**/*Service.ts

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Service files must include Service suffix, use PascalCase matching exported class, and avoid generic names (e.g., MembershipService.ts)

Files:

  • packages/lib/setupPaymentService.ts
🧠 Learnings (2)
📚 Learning: 2025-07-28T11:50:23.946Z
Learnt from: CR
PR: calcom/cal.com#0
File: .cursor/rules/review.mdc:0-0
Timestamp: 2025-07-28T11:50:23.946Z
Learning: Applies to **/*.ts : Ensure the `credential.key` field is never returned from tRPC endpoints or APIs

Applied to files:

  • packages/app-store/_utils/prepareAppsWithCredentials.ts
📚 Learning: 2025-08-08T09:12:08.280Z
Learnt from: hariombalhara
PR: calcom/cal.com#22968
File: packages/features/auth/lib/next-auth-options.ts:327-327
Timestamp: 2025-08-08T09:12:08.280Z
Learning: In packages/features/auth/lib/next-auth-options.ts, do not log credentials in authorize() handlers (e.g., the "saml-idp" CredentialsProvider). Remove accidental console.log statements and avoid including credential contents in logs; prefer either no logging or structured logs without sensitive data.

Applied to files:

  • packages/app-store/_utils/prepareAppsWithCredentials.ts
🧬 Code graph analysis (2)
packages/app-store/_utils/prepareAppsWithCredentials.ts (2)
packages/app-store/locations.ts (1)
  • EventLocationType (63-63)
packages/lib/piiFreeData.ts (1)
  • getPiiFreeCredential (61-72)
packages/lib/setupPaymentService.ts (2)
packages/app-store/_utils/prepareAppsWithCredentials.ts (2)
  • PreparedApp (22-26)
  • CredentialDataWithTeamName (16-20)
packages/app-store/_utils/payments/paymentLoaders.ts (1)
  • PaymentLoaderKey (16-16)
🪛 Biome (2.1.2)
packages/lib/setupPaymentService.ts

[error] 16-16: Unsafe usage of optional chaining.

If it short-circuits with 'undefined' the evaluation will throw TypeError here:

(lint/correctness/noUnsafeOptionalChaining)

⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Type check / check-types
  • GitHub Check: Linters / lint
  • GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (4)
packages/app-store/_utils/prepareAppsWithCredentials.ts (4)

66-73: Location option derivation LGTM

Derives locationOption only when the app has credentials (including globals). This keeps UI options relevant. No change requested.


1-3: Import usage LGTM

getPiiFreeCredential and safeStringify are used appropriately for logging; once you apply the sanitization patch, getPiiFreeCredential will also serve the payload.


9-14: Type definition for LocationOption LGTM

Concise and matches usage; no unnecessary fields.


16-20: CredentialDataWithTeamName shape LGTM as an internal/source type

This remains useful as the input to the function. After switching the public return type to a sanitized form, keep this type for internal filtering and injection logic.

@zhyd1997
Copy link
Contributor Author

zhyd1997 commented Aug 27, 2025

Thanks for reviewing this!

Although with the current release I was able to get the compile time down to 7.3s on my machine, I think splitting the heavy service is a better and clearer direction. If we can continue generating the video and payment service maps, the results should be even better.

Closing this PR in favor of that approach.

@zhyd1997 zhyd1997 closed this Aug 27, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app-store area: app store, apps, calendar integrations, google calendar, outlook, lark, apple calendar community Created by Linear-GitHub Sync performance area: performance, page load, slow, slow endpoints, loading screen, unresponsive

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants