Skip to content

Comments

feat: Add async spam check and decoy booking response#24326

Merged
alishaz-polymath merged 6 commits intomainfrom
devin/spam-async-check-1759840597
Oct 14, 2025
Merged

feat: Add async spam check and decoy booking response#24326
alishaz-polymath merged 6 commits intomainfrom
devin/spam-async-check-1759840597

Conversation

@hariombalhara
Copy link
Member

@hariombalhara hariombalhara commented Oct 7, 2025

What does this PR do?

This PR implements an async spam check feature that runs in parallel with booking creation to detect and block spam bookings with zero performance impact for legitimate users. When spam is detected, the system generates a fake booking response and redirects spammers to a convincing decoy success page.

Requested by: @hariombalhara
Link to Devin run: https://app.devin.ai/sessions/52c340ed352645dca2bf9d218111b36d


Key Features

1. Parallel Spam Check (Zero Delay)

  • Spam check starts immediately when handleNewBooking begins
  • Runs in parallel with availability loading and event processing
  • Result is awaited only before booking creation
  • No performance impact for legitimate users

2. Decoy Success Page for Spammers

  • New route: /booking-successful/[localStorageUid]
  • Stores booking data in localStorage with 24-hour expiration
  • Shows convincing confirmation page to spammers
  • Uses same UI components as real success page

3. SpamCheckService Implementation

  • Orchestrates spam checking across global and organization-specific watchlists
  • Provides startCheck() and waitForCheck() async methods
  • Fail-open behavior (allows booking if error occurs)

4. DI Container Architecture Fixes

  • Updated service creation to use facade pattern (aligns with base branch)
  • Services now receive dependencies as named properties in Deps objects
  • Fixed import paths for proper type handling (IAuditRepository)

Files Changed

New Files

  • apps/web/app/(booking-page-wrapper)/booking-successful/[localStorageUid]/page.tsx - Decoy success page
  • packages/features/watchlist/lib/service/SpamCheckService.ts - Spam check orchestration service
  • spam-check-architecture.md - Architecture documentation
  • spam-check-flow.mermaid - Flow diagram

Modified Files

  • packages/features/bookings/lib/handleNewBooking.ts - Async spam check integration
  • packages/features/bookings/Booker/components/hooks/useBookings.ts - Decoy page redirect logic
  • packages/features/di/watchlist/containers/watchlist.ts - Facade pattern service creation
  • packages/features/di/watchlist/containers/SpamCheckService.container.ts - DI wiring
  • packages/features/di/watchlist/modules/Watchlist.module.ts - Removed incorrect service bindings

⚠️ Important Review Areas

1. Fake Booking Generation

When spam is detected, handleNewBooking.ts generates a fake booking response:

if (spamResult.isBlocked) {
  return {
    ...fakeBookingData,
    isShortCircuitedBooking: true,
  };
}

Review: Ensure no side effects occur (no emails sent, no webhooks triggered, no DB writes).

2. localStorage Data Storage

The decoy success page stores booking data in localStorage:

const bookingSuccessData = {
  booking: {
    title, startTime, endTime,
    booker: { name, email },
    host: { name },
    location,
  },
  timestamp: Date.now(),
};

Review: Consider privacy implications of storing attendee email in localStorage.

3. Async Timing

The spam check runs in parallel:

spamCheckService.startCheck(email, organizationId); // Start async
// ... other processing ...
const spamResult = await spamCheckService.waitForCheck(); // Wait before booking creation

Review: Verify no race conditions or timing issues.


How should this be tested?

Testing Spam Detection

  1. Add an email to the global watchlist: spam@example.com
  2. Try to book with that email
  3. Verify you see the decoy success page at /booking-successful/[uuid]
  4. Verify no actual booking was created in the database
  5. Verify no emails were sent

Testing Organization Watchlist

  1. Add an email to an organization's watchlist
  2. Try to book an event in that organization with the blocked email
  3. Verify decoy success page appears
  4. Try to book an event in a different organization with the same email
  5. Verify booking succeeds normally

Testing Legitimate Users

  1. Book with a non-blocked email
  2. Verify normal booking flow works
  3. Verify no performance degradation

Testing Fail-Open Behavior

  1. Simulate spam check service failure
  2. Verify booking proceeds normally (fail-open)

Mandatory Tasks

  • I have self-reviewed the code
  • I have updated the developer docs (N/A - feature is internal)
  • ⚠️ I confirm automated tests are in place (NO NEW TESTS ADDED - see concerns below)

Checklist

  • My code follows the style guidelines of this project
  • I have commented my code in hard-to-understand areas
  • I have checked that my changes generate no new warnings (in local lint)
  • All CI checks passed (11 passed, 0 failed)

@devin-ai-integration
Copy link
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@keithwillcode keithwillcode added core area: core, team members only enterprise area: enterprise, audit log, organisation, SAML, SSO labels Oct 7, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 7, 2025

Walkthrough

Adds spam-blocking into the booking flow: a new SpamCheckService with DI bindings and a factory is introduced and invoked from handleNewBooking to start and await a spam check. When blocked, handlers return a short-circuited decoy booking (flagged with isShortCircuitedBooking) instead of persisting the real booking. Client-side support stores decoy data in localStorage (store/get/remove), exposes a useDecoyBooking hook, and renders decoy success UI via DecoyBookingSuccessCard and a booking-successful/[uid] page. useBookings persists decoys and redirects to the success route. Tests and minor DB select changes were added.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly and accurately describes the core feature introduced by this changeset—adding an asynchronous spam check and decoy booking response—and directly matches the primary changes in handleNewBooking, SpamCheckService, and the decoy success UI. It is concise, clear, and uses terminology consistent with the code changes. A teammate scanning history will immediately understand the main purpose of the PR.
Description Check ✅ Passed The pull request description thoroughly outlines the implementation of the async spam check feature and decoy booking response, enumerates key files added and modified, and specifies testing scenarios and review focus areas that align with the actual code changes. It remains focused on the PR’s scope and provides relevant context without deviating from the changeset.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch devin/spam-async-check-1759840597

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • 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 a1dd9e5 and 2fb5a86.

📒 Files selected for processing (1)
  • packages/features/bookings/lib/handleNewBooking.ts (5 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/features/bookings/lib/handleNewBooking.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/features/bookings/lib/handleNewBooking.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/features/bookings/lib/handleNewBooking.ts
🧠 Learnings (1)
📚 Learning: 2025-09-03T09:52:51.182Z
Learnt from: hariombalhara
PR: calcom/cal.com#23541
File: packages/features/bookings/lib/di/modules/RegularBookingServiceModule.ts:22-28
Timestamp: 2025-09-03T09:52:51.182Z
Learning: The IBookingServiceDependencies interface in packages/features/bookings/lib/handleNewBooking.ts contains 6 properties: cacheService, checkBookingAndDurationLimitsService, prismaClient, bookingRepository, featuresRepository, and checkBookingLimitsService. This interface is used by RegularBookingService and matches the depsMap structure in RegularBookingServiceModule.

Applied to files:

  • packages/features/bookings/lib/handleNewBooking.ts
🧬 Code graph analysis (1)
packages/features/bookings/lib/handleNewBooking.ts (4)
packages/features/bookings/lib/handleNewBooking/createBooking.ts (1)
  • Booking (347-347)
packages/types/Calendar.d.ts (1)
  • AppsStatus (141-148)
packages/features/di/watchlist/containers/SpamCheckService.container.ts (1)
  • getSpamCheckService (7-11)
packages/features/eventtypes/lib/eventNaming.ts (1)
  • getEventName (31-113)
⏰ 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). (8)
  • GitHub Check: Production builds / Build API v1
  • GitHub Check: Production builds / Build API v2
  • GitHub Check: Production builds / Build Docs
  • GitHub Check: Production builds / Build Atoms
  • GitHub Check: Production builds / Build Web App
  • GitHub Check: Linters / lint
  • GitHub Check: Type check / check-types
  • GitHub Check: Tests / Unit
🔇 Additional comments (4)
packages/features/bookings/lib/handleNewBooking.ts (4)

37-37: LGTM!

Clean import of the spam check service from the DI container. The service is properly utilized at line 511 to initiate the async spam check.


158-160: LGTM!

The isShortCircuitedBooking flag appropriately marks decoy bookings without revealing spam detection details to potential attackers.


511-516: LGTM! Optimal timing for async spam check.

Starting the spam check early (before availability checks) minimizes latency impact on legitimate users. The organization ID derivation from the team hierarchy is correct.

If not already implemented within SpamCheckService.waitForCheck(), consider adding explicit error handling here to ensure fail-open behavior (allow booking on check errors) is clear:

const spamCheckService = getSpamCheckService();
// Either it is a team event or a managed child event of a managed event
const team = eventType.team ?? eventType.parent?.team ?? null;
const eventOrganizationId = team?.parentId ?? null;
spamCheckService.startCheck({ email: bookerEmail, organizationId: eventOrganizationId });

Based on PR objectives:

A new SpamCheckService orchestrates checks... employing fail-open behavior on errors.

1416-1427: LGTM! Correct placement and decoy structure.

The spam check is awaited at the optimal point—after availability checks but before booking creation. This avoids impacting legitimate users while preventing side effects for blocked bookings. The random organizer names (line 1419-1421) and null user email (line 1437) provide appropriate decoy authenticity.


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

Comment @coderabbitai help to get the list of available commands and usage tips.

@pull-request-size pull-request-size bot removed the size/L label Oct 7, 2025
@vercel
Copy link

vercel bot commented Oct 7, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Preview Comments Updated (UTC)
cal Ignored Ignored Oct 14, 2025 11:02am
cal-eu Ignored Ignored Oct 14, 2025 11:02am

@hariombalhara hariombalhara force-pushed the devin/spam-async-check-1759840597 branch from 57db386 to eb9900b Compare October 8, 2025 05:43
@hariombalhara hariombalhara force-pushed the devin/spam-async-check-1759840597 branch from 89258f0 to 649e0c2 Compare October 8, 2025 07:23
devin-ai-integration bot added a commit that referenced this pull request Oct 8, 2025
…am exploitation

- Generate unique UID for localStorage key on booking success
- Store booking display data in localStorage instead of query params
- Redirect to /booking-successful/{uid} route
- Read from localStorage and redirect to actual booking page
- Clean up localStorage after reading to prevent stale data
- Add 24-hour expiration for localStorage data
- Preserve custom successRedirectUrl behavior for external redirects
- Support embed mode with dedicated embed route

This prevents malicious actors from crafting fake booking confirmation
pages by manipulating query parameters.

Stacked on PR #24326

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>
@hariombalhara hariombalhara force-pushed the devin/spam-async-check-1759840597 branch 2 times, most recently from 66c1811 to 1ed7cc6 Compare October 8, 2025 12:53
@hariombalhara hariombalhara changed the title feat: Add async spam check with parallel execution and fake booking redirect feat: Add async spam check and decoy booking response Oct 9, 2025
@hariombalhara hariombalhara changed the title feat: Add async spam check and decoy booking response feat: Add async spam check and decoy booking response[Stacked] Oct 13, 2025
@hariombalhara hariombalhara marked this pull request as ready for review October 13, 2025 12:46
@hariombalhara hariombalhara requested a review from a team October 13, 2025 12:46
@hariombalhara hariombalhara requested a review from a team as a code owner October 13, 2025 12:46
@graphite-app graphite-app bot requested a review from a team October 13, 2025 12:46
- Integrate SpamCheckService with handleNewBooking workflow
- Implement parallel spam check execution for minimal performance impact
- Add decoy booking response with localStorage-based success page
- Extract organization ID from event type for org-specific blocking
- Add comprehensive test coverage for spam detection scenarios
- Create reusable components for booking success cards
- Implement fail-open behavior to never block legitimate bookings

This builds on the spam blocker DI infrastructure from PR #24040 by
adding the actual integration into the booking flow and implementing
the decoy response mechanism to avoid revealing spam detection to
malicious actors.

Related: #24040
Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>
@hariombalhara hariombalhara force-pushed the devin/spam-async-check-1759840597 branch from 7a5f6f9 to bd229ca Compare October 14, 2025 09:18
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: 5

♻️ Duplicate comments (4)
packages/features/bookings/lib/client/decoyBookingStore.ts (2)

26-33: Type safety concern: overly permissive signature allows arbitrary data storage.

The Record<string, unknown> type accepts any object structure, making it easy to accidentally store sensitive data beyond what's needed for the decoy UI. While the past review comment addressed limiting stored fields in the calling code (useBookings.ts), the store function itself provides no safeguards.

Consider tightening the signature to match DecoyBookingData or creating a dedicated input type:

+interface StoreDecoyBookingInput {
+  uid: string;
+  title: string | null;
+  startTime: string;
+  endTime: string;
+  booker: { name: string; timeZone?: string; email: string } | null;
+  host: { name: string; timeZone?: string } | null;
+  location: string | null;
+}
+
-export function storeDecoyBooking(booking: Record<string, unknown> & { uid: string }): void {
+export function storeDecoyBooking(booking: StoreDecoyBookingInput): void {

56-65: PII retention risk: 24-hour TTL and missing consume-on-read.

Storing names/emails in localStorage for 24 hours poses a privacy risk (XSS exfiltration). The data should be removed immediately upon successful retrieval, and the TTL should be reduced to minimize exposure.

This mirrors the past review comment. Apply the suggested changes to reduce TTL to 1 hour (or less) and implement consume-on-read:

-    const maxAge = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
+    const maxAge = 60 * 60 * 1000; // 1 hour in milliseconds

     if (dataAge > maxAge) {
       localStorage.removeItem(storageKey);
       return null;
     }

-    return data;
+    // Consume-on-read to minimize PII exposure
+    localStorage.removeItem(storageKey);
+    return data;
packages/features/bookings/Booker/components/DecoyBookingSuccessCard.tsx (1)

37-42: Accessibility blocker: aria-hidden hides dialog from screen readers.

The past review comment marked this as addressed, but aria-hidden="true" is still present on line 37, hiding the entire dialog and its children from assistive technology. This prevents screen reader users from accessing the booking confirmation content.

Apply the previously suggested fix:

-            <div className="main inset-0 my-4 flex flex-col transition-opacity sm:my-0" aria-hidden="true">
+            <div className="main inset-0 my-4 flex flex-col transition-opacity sm:my-0">
               <div
                 className="bg-default dark:bg-muted border-booker border-booker-width inline-block transform overflow-hidden rounded-lg px-8 pb-4 pt-5 text-left align-bottom transition-all sm:my-8 sm:w-full sm:max-w-xl sm:py-8 sm:align-middle"
                 role="dialog"
                 aria-modal="true"
                 aria-labelledby="modal-headline">

If a backdrop dimming effect is needed, add a separate overlay div with aria-hidden instead of hiding the dialog container.

packages/features/bookings/lib/handleNewBooking/test/spam-booking.integration-test.ts (1)

374-399: Mock doesn't intercept the service instance used by handler.

This test creates a spy on a single service instance, but handleNewBooking calls getSpamCheckService() again, creating a new instance that bypasses your spy. The fail-open behavior is not actually being tested.

As noted in the previous review, you need to either:

  • Option A: Mock the container module so getSpamCheckService() returns your mocked service
  • Option B: Spy on SpamCheckService.prototype to intercept all instances

The current approach creates a false positive—the test passes even though it's not actually verifying fail-open behavior.

Refer to the detailed solutions in the previous review comment above.

🧹 Nitpick comments (2)
packages/features/di/watchlist/containers/SpamCheckService.container.ts (1)

7-10: Remove redundant type assertions.

The type assertions as GlobalBlockingService and as OrganizationBlockingService are unnecessary since the getter functions already return the correct types (as declared in watchlist.ts lines 41 and 48).

Apply this diff:

 export const getSpamCheckService = (): SpamCheckService => {
-  const globalBlockingService = getGlobalBlockingService() as GlobalBlockingService;
-  const organizationBlockingService = getOrganizationBlockingService() as OrganizationBlockingService;
+  const globalBlockingService = getGlobalBlockingService();
+  const organizationBlockingService = getOrganizationBlockingService();
   return new SpamCheckService(globalBlockingService, organizationBlockingService);
 };
packages/features/bookings/lib/handleNewBooking/test/spam-booking.integration-test.ts (1)

39-39: Simplify boolean logic.

The ternary expression can be simplified to a direct comparison.

Apply this diff:

-      isGlobal: overrides.organizationId !== null ? false : true,
+      isGlobal: overrides.organizationId === null,
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • 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 0feffd9 and 7a5f6f9.

📒 Files selected for processing (13)
  • apps/web/app/(booking-page-wrapper)/booking-successful/[uid]/page.tsx (1 hunks)
  • apps/web/test/utils/bookingScenario/bookingScenario.ts (1 hunks)
  • packages/features/bookings/Booker/components/DecoyBookingSuccessCard.tsx (1 hunks)
  • packages/features/bookings/Booker/components/hooks/useBookings.ts (7 hunks)
  • packages/features/bookings/Booker/components/hooks/useDecoyBooking.ts (1 hunks)
  • packages/features/bookings/components/BookingSuccessCard.tsx (1 hunks)
  • packages/features/bookings/lib/client/decoyBookingStore.ts (1 hunks)
  • packages/features/bookings/lib/handleNewBooking.ts (4 hunks)
  • packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts (1 hunks)
  • packages/features/bookings/lib/handleNewBooking/test/spam-booking.integration-test.ts (1 hunks)
  • packages/features/di/watchlist/containers/SpamCheckService.container.ts (1 hunks)
  • packages/features/di/watchlist/containers/watchlist.ts (2 hunks)
  • packages/features/watchlist/lib/service/SpamCheckService.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/web/test/utils/bookingScenario/bookingScenario.ts
  • packages/features/bookings/components/BookingSuccessCard.tsx
  • packages/features/watchlist/lib/service/SpamCheckService.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/features/di/watchlist/containers/SpamCheckService.container.ts
  • packages/features/di/watchlist/containers/watchlist.ts
  • packages/features/bookings/lib/handleNewBooking/test/spam-booking.integration-test.ts
  • packages/features/bookings/Booker/components/hooks/useDecoyBooking.ts
  • packages/features/bookings/lib/handleNewBooking.ts
  • packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts
  • packages/features/bookings/lib/client/decoyBookingStore.ts
  • packages/features/bookings/Booker/components/hooks/useBookings.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/features/di/watchlist/containers/SpamCheckService.container.ts
  • packages/features/bookings/Booker/components/DecoyBookingSuccessCard.tsx
  • apps/web/app/(booking-page-wrapper)/booking-successful/[uid]/page.tsx
  • packages/features/di/watchlist/containers/watchlist.ts
  • packages/features/bookings/lib/handleNewBooking/test/spam-booking.integration-test.ts
  • packages/features/bookings/Booker/components/hooks/useDecoyBooking.ts
  • packages/features/bookings/lib/handleNewBooking.ts
  • packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts
  • packages/features/bookings/lib/client/decoyBookingStore.ts
  • packages/features/bookings/Booker/components/hooks/useBookings.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/features/di/watchlist/containers/SpamCheckService.container.ts
  • packages/features/bookings/Booker/components/DecoyBookingSuccessCard.tsx
  • apps/web/app/(booking-page-wrapper)/booking-successful/[uid]/page.tsx
  • packages/features/di/watchlist/containers/watchlist.ts
  • packages/features/bookings/lib/handleNewBooking/test/spam-booking.integration-test.ts
  • packages/features/bookings/Booker/components/hooks/useDecoyBooking.ts
  • packages/features/bookings/lib/handleNewBooking.ts
  • packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts
  • packages/features/bookings/lib/client/decoyBookingStore.ts
  • packages/features/bookings/Booker/components/hooks/useBookings.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/features/bookings/Booker/components/DecoyBookingSuccessCard.tsx
  • apps/web/app/(booking-page-wrapper)/booking-successful/[uid]/page.tsx
🧠 Learnings (4)
📚 Learning: 2025-09-03T09:52:51.182Z
Learnt from: hariombalhara
PR: calcom/cal.com#23541
File: packages/features/bookings/lib/di/modules/RegularBookingServiceModule.ts:22-28
Timestamp: 2025-09-03T09:52:51.182Z
Learning: The IBookingServiceDependencies interface in packages/features/bookings/lib/handleNewBooking.ts contains 6 properties: cacheService, checkBookingAndDurationLimitsService, prismaClient, bookingRepository, featuresRepository, and checkBookingLimitsService. This interface is used by RegularBookingService and matches the depsMap structure in RegularBookingServiceModule.

Applied to files:

  • packages/features/bookings/lib/handleNewBooking.ts
📚 Learning: 2025-08-21T13:44:06.805Z
Learnt from: supalarry
PR: calcom/cal.com#23217
File: apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts:93-94
Timestamp: 2025-08-21T13:44:06.805Z
Learning: In apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts, repository functions that use explicit Prisma select clauses (like getEventTypeWithSeats) are used for specific purposes and don't need to include all EventType fields like bookingRequiresAuthentication. These methods don't feed into the general OutputEventTypesService_2024_06_14 flow.

Applied to files:

  • packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts
📚 Learning: 2025-09-03T11:54:05.409Z
Learnt from: supalarry
PR: calcom/cal.com#23514
File: apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts:579-582
Timestamp: 2025-09-03T11:54:05.409Z
Learning: In calcom/cal.com bookings repository methods, when Prisma include uses `eventType: true`, all eventType fields including seatsShowAttendees are automatically included in the selection. Explicit field selection is not required when using `true` for nested relations.

Applied to files:

  • packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts
📚 Learning: 2025-09-03T11:54:05.409Z
Learnt from: supalarry
PR: calcom/cal.com#23514
File: apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts:579-582
Timestamp: 2025-09-03T11:54:05.409Z
Learning: In calcom/cal.com bookings repository methods, when Prisma select uses `eventType: true`, all eventType fields including seatsShowAttendees are automatically included in the selection. Explicit field selection is not required when using `true` for nested relations.

Applied to files:

  • packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts
🧬 Code graph analysis (8)
packages/features/di/watchlist/containers/SpamCheckService.container.ts (4)
packages/features/watchlist/lib/service/SpamCheckService.ts (1)
  • SpamCheckService (14-57)
packages/features/di/watchlist/containers/watchlist.ts (2)
  • getGlobalBlockingService (41-46)
  • getOrganizationBlockingService (48-53)
packages/features/watchlist/lib/service/GlobalBlockingService.ts (1)
  • GlobalBlockingService (14-49)
packages/features/watchlist/lib/service/OrganizationBlockingService.ts (1)
  • OrganizationBlockingService (14-59)
apps/web/app/(booking-page-wrapper)/booking-successful/[uid]/page.tsx (2)
packages/features/bookings/Booker/components/hooks/useDecoyBooking.ts (1)
  • useDecoyBooking (14-30)
packages/features/bookings/Booker/components/DecoyBookingSuccessCard.tsx (1)
  • DecoyBookingSuccessCard (18-114)
packages/features/di/watchlist/containers/watchlist.ts (6)
packages/features/watchlist/lib/service/GlobalBlockingService.ts (1)
  • GlobalBlockingService (14-49)
packages/features/watchlist/lib/interface/IWatchlistRepositories.ts (2)
  • IGlobalWatchlistRepository (9-36)
  • IOrganizationWatchlistRepository (42-79)
packages/features/di/watchlist/Watchlist.tokens.ts (1)
  • WATCHLIST_DI_TOKENS (1-12)
packages/features/watchlist/lib/service/OrganizationBlockingService.ts (1)
  • OrganizationBlockingService (14-59)
packages/features/watchlist/lib/service/WatchlistAuditService.ts (1)
  • WatchlistAuditService (13-44)
packages/features/watchlist/lib/interface/IAuditRepository.ts (1)
  • IAuditRepository (20-33)
packages/features/bookings/lib/handleNewBooking/test/spam-booking.integration-test.ts (3)
packages/features/bookings/lib/handleNewBooking/test/getNewBookingHandler.ts (1)
  • getNewBookingHandler (15-17)
apps/web/test/utils/bookingScenario/bookingScenario.ts (9)
  • getBooker (2250-2264)
  • getOrganizer (1539-1598)
  • TestData (1258-1530)
  • getGoogleCalendarCredential (1211-1219)
  • createBookingScenario (997-1028)
  • getScenarioData (1600-1688)
  • mockCalendarToHaveNoBusySlots (1989-2002)
  • mockCalendarToCrashOnGetAvailability (2004-2006)
  • createOrganization (1040-1079)
packages/features/di/watchlist/containers/SpamCheckService.container.ts (1)
  • getSpamCheckService (7-11)
packages/features/bookings/Booker/components/hooks/useDecoyBooking.ts (1)
packages/features/bookings/lib/client/decoyBookingStore.ts (2)
  • DecoyBookingData (86-86)
  • getDecoyBooking (40-71)
packages/features/bookings/lib/handleNewBooking.ts (1)
packages/features/di/watchlist/containers/SpamCheckService.container.ts (1)
  • getSpamCheckService (7-11)
packages/features/bookings/lib/client/decoyBookingStore.ts (1)
packages/lib/webstorage.ts (1)
  • localStorage (6-36)
packages/features/bookings/Booker/components/hooks/useBookings.ts (2)
packages/features/bookings/lib/client/decoyBookingStore.ts (1)
  • storeDecoyBooking (26-33)
packages/ui/components/toast/showToast.tsx (1)
  • showToast (77-101)
⏰ 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: Tests / Unit
  • GitHub Check: Linters / lint
  • GitHub Check: Type check / check-types
🔇 Additional comments (5)
packages/features/di/watchlist/containers/watchlist.ts (1)

41-57: LGTM! DI wiring correctly implements repository-based service construction.

The refactoring from service tokens to repository-based construction aligns with the facade pattern mentioned in the PR summary. Each getter correctly:

  1. Retrieves the appropriate repository from the container
  2. Constructs the service with the expected dependency structure
  3. Returns a properly typed instance

The dependency keys match the service constructors' expectations (globalRepo, orgRepo, auditRepository).

packages/features/bookings/lib/handleNewBooking.ts (2)

509-513: LGTM! Spam check correctly initiated with organizational context.

The spam check is started early in the booking flow (before expensive operations) and correctly determines the organizational context from the event type's team hierarchy. The organizationId derivation (team?.parentId ?? null) appropriately reflects either the team's parent organization or null for non-organizational events.


1441-1447: Consider privacy implications: spammer's email stored in decoy attendees.

The decoy booking includes the blocked user's email in the attendees array (line 1443), which flows through to localStorage via storeDecoyBooking. While this provides a realistic decoy experience, it means potentially malicious emails are persisted client-side for up to 24 hours.

Please confirm the design intent:

  1. Is storing the blocked email client-side acceptable for the decoy strategy?
  2. Should the email be masked (e.g., b***@domain.com) or omitted entirely in the decoy response?
  3. Does client-side storage of blocked emails create any compliance concerns (e.g., logging PII of users who trigger spam detection)?

The current implementation prioritizes a realistic decoy (showing the booker's own email back to them), but consider:

  • Spammers could inspect localStorage and discover their email was stored without a real booking
  • If the email is already blocked server-side, the client doesn't need it for the decoy UI

Based on learnings: The broader PII-in-localStorage concern has been flagged multiple times in this PR. Evaluate whether the attendee email is necessary in the decoy booking data stored client-side.

packages/features/bookings/Booker/components/hooks/useBookings.ts (1)

287-295: PII minimization confirmed: booking.attendees[0] and booking.user include only email, name, and timeZone, so no additional PII is stored.

packages/features/bookings/lib/handleNewBooking/test/spam-booking.integration-test.ts (1)

405-788: LGTM! Comprehensive organization watchlist test coverage.

The organization watchlist tests are well-structured and cover critical scenarios:

  • Email and domain blocking within org context
  • Cross-organization isolation (ensuring one org's watchlist doesn't block another org's bookings)
  • Managed event types (child events inherit parent's org context)

The tests properly verify both the decoy response shape and the absence of database records for blocked bookings.

BookingLocations,
createOrganization,
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
import { prisma } from "@calcom/prisma"
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 | 🟡 Minor

Add missing semicolon.

Add semicolon after the import statement for consistency.

Apply this diff:

-import { prisma } from "@calcom/prisma"
+import { prisma } from "@calcom/prisma";
📝 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
import { prisma } from "@calcom/prisma"
import { prisma } from "@calcom/prisma";
🤖 Prompt for AI Agents
In
packages/features/bookings/lib/handleNewBooking/test/spam-booking.integration-test.ts
around line 13, the import statement "import { prisma } from \"@calcom/prisma\""
is missing a trailing semicolon; add a semicolon at the end of that import line
to match project formatting and consistency.

action: "BLOCK",
});

console.log("watchlists", await prisma.watchlist.findMany());
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 | 🟡 Minor

Remove debug console.log statement.

This appears to be a debugging artifact that should be removed before merging.

Apply this diff:

-        console.log("watchlists", await prisma.watchlist.findMany());
-
📝 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
console.log("watchlists", await prisma.watchlist.findMany());
🤖 Prompt for AI Agents
In
packages/features/bookings/lib/handleNewBooking/test/spam-booking.integration-test.ts
around line 124, remove the debug console.log("watchlists", await
prisma.watchlist.findMany()); statement; delete the line entirely (or replace
with an appropriate test assertion or use the test logger if you need
visibility) so no debug console output remains in the committed test.

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: 2

♻️ Duplicate comments (7)
packages/features/bookings/Booker/components/hooks/useBookings.ts (1)

281-300: Minimize PII in localStorage for decoy bookings; store only needed fields

Don’t persist full attendees[0] and user objects. Store minimal fields (title, times, host/attendee names/emails, location) to reduce PII exposure. Also type the payload.

Apply this diff:

-        const bookingData = {
-          uid: booking.uid,
-          title: booking.title ?? null,
-          startTime: booking.startTime,
-          endTime: booking.endTime,
-          booker: booking.attendees?.[0] ?? null,
-          host: booking.user ?? null,
-          location: booking.location ?? null,
-        };
-
-        storeDecoyBooking(bookingData);
+        const bookingData: {
+          uid: string;
+          title: string | null;
+          startTime: string | Date;
+          endTime: string | Date;
+          hostName: string | null;
+          hostEmail: string | null;
+          attendeeName: string | null;
+          attendeeEmail: string | null;
+          location: string | null;
+        } = {
+          uid: booking.uid,
+          title: booking.title ?? null,
+          startTime: booking.startTime,
+          endTime: booking.endTime,
+          hostName: booking.user?.name ?? null,
+          hostEmail: booking.user?.email ?? null,
+          attendeeName: booking.attendees?.[0]?.name ?? null,
+          attendeeEmail: booking.attendees?.[0]?.email ?? null,
+          location: booking.location ?? null,
+        };
+
+        storeDecoyBooking(bookingData as unknown as Record<string, unknown> & { uid: string });
         router.push(`/booking-successful/${booking.uid}`);

Additionally, ensure decoy entry is removed after rendering on the success page/hook. Based on learnings

packages/features/bookings/components/BookingSuccessCard.tsx (1)

37-43: Accessibility: remove aria-hidden from dialog wrapper; add a separate overlay

aria-hidden="true" on the wrapper hides the dialog subtree from screen readers. Keep overlay as separate element and remove aria-hidden from the wrapper.

Apply:

-            <div className="main inset-0 my-4 flex flex-col transition-opacity sm:my-0" aria-hidden="true">
+            {/* Overlay */}
+            <div className="fixed inset-0 bg-black/50" aria-hidden="true" />
+            <div className="main inset-0 my-4 flex flex-col transition-opacity sm:my-0">
               <div
                 className="bg-default dark:bg-muted border-booker border-booker-width inline-block transform overflow-hidden rounded-lg px-8 pb-4 pt-5 text-left align-bottom transition-all sm:my-8 sm:w-full sm:max-w-xl sm:py-8 sm:align-middle"
                 role="dialog"
                 aria-modal="true"
                 aria-labelledby="modal-headline">
apps/web/app/(booking-page-wrapper)/booking-successful/[uid]/page.tsx (1)

31-34: Misleading comment; attendee email is displayed

Comment claims email isn’t stored, but you render booking.booker?.email. Fix the comment or set attendeeEmail to null if intentionally not showing.

-  const hostEmail = null; // Email not stored for spam decoy bookings
+  const hostEmail = null; // Host email intentionally omitted from decoy display
packages/features/bookings/Booker/components/DecoyBookingSuccessCard.tsx (1)

37-43: Accessibility: remove aria-hidden from dialog wrapper; add an overlay

Same issue as the main card: aria-hidden on the wrapper hides the dialog.

Apply:

-            <div className="main inset-0 my-4 flex flex-col transition-opacity sm:my-0" aria-hidden="true">
+            {/* Overlay */}
+            <div className="fixed inset-0 bg-black/50" aria-hidden="true" />
+            <div className="main inset-0 my-4 flex flex-col transition-opacity sm:my-0">
               <div
                 className="bg-default dark:bg-muted border-booker border-booker-width inline-block transform overflow-hidden rounded-lg px-8 pb-4 pt-5 text-left align-bottom transition-all sm:my-8 sm:w-full sm:max-w-xl sm:py-8 sm:align-middle"
                 role="dialog"
                 aria-modal="true"
                 aria-labelledby="modal-headline">
packages/features/bookings/lib/handleNewBooking/test/spam-booking.integration-test.ts (2)

13-13: Add missing semicolon on import.

Update the prisma import to include a semicolon for consistency.

-import { prisma } from "@calcom/prisma"
+import { prisma } from "@calcom/prisma";

124-124: Remove debug console.log from tests.

Leftover debug output should be removed.

-        console.log("watchlists", await prisma.watchlist.findMany());
packages/features/watchlist/lib/service/SpamCheckService.ts (1)

29-36: Enforce fail-open with timeout; don’t throw when not started.

waitForCheck can hang and currently throws if startCheck wasn’t called. Race with a short timeout and default to not-blocked; return fail‑open when no promise.

-  async waitForCheck(): Promise<BlockingResult> {
-    if (!this.spamCheckPromise) {
-      throw new Error(
-        "waitForCheck() called before startCheck(). You must call startCheck() first to initialize spam checking."
-      );
-    }
-    return await this.spamCheckPromise;
-  }
+  async waitForCheck(timeoutMs = 300): Promise<BlockingResult> {
+    if (!this.spamCheckPromise) return { isBlocked: false };
+    const timeout = new Promise<BlockingResult>((resolve) =>
+      setTimeout(() => resolve({ isBlocked: false }), timeoutMs)
+    );
+    return Promise.race([this.spamCheckPromise, timeout]);
+  }
🧹 Nitpick comments (4)
packages/features/di/watchlist/modules/Watchlist.module.ts (1)

14-20: Bindings look correct; minor style nit

Bindings to blocking services with named deps align with service constructors. Consider spacing for consistency.

-watchlistModule.bind(WATCHLIST_DI_TOKENS.ORGANIZATION_BLOCKING_SERVICE).toClass(OrganizationBlockingService,{
+watchlistModule
+  .bind(WATCHLIST_DI_TOKENS.ORGANIZATION_BLOCKING_SERVICE)
+  .toClass(OrganizationBlockingService, {
     orgRepo: DI_TOKENS.ORGANIZATION_WATCHLIST_REPOSITORY,
   });
packages/features/bookings/lib/handleNewBooking.ts (1)

1414-1453: Decoy short‑circuit before side effects

You return before DB writes, emails, webhooks. This prevents side effects for blocked bookings. Small suggestions:

  • Consider localizing DECOY_ORGANIZER_NAMES or moving to a shared constant.
  • Ensure the returned shape stays stable (uid, status, attendees) for client handling.
packages/features/watchlist/lib/service/SpamCheckService.ts (1)

48-53: Safer orgId check.

Use explicit null/undefined check to avoid edge cases with falsy numeric IDs.

-    if (organizationId) {
+    if (organizationId != null) {
       const orgResult = await this.organizationBlockingService.isBlocked(email, organizationId);
       if (orgResult.isBlocked) {
         return orgResult;
       }
     }
packages/features/bookings/lib/handleNewBooking/test/spam-booking.integration-test.ts (1)

167-169: Mask or omit attendee email in decoy booking storage and tests
storeDecoyBooking persists full attendee.email to localStorage—which embeds PII client-side; refactor to mask or remove the email and update tests to assert on attendee shape or masked value instead.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • 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 7a5f6f9 and bd229ca.

📒 Files selected for processing (13)
  • apps/web/app/(booking-page-wrapper)/booking-successful/[uid]/page.tsx (1 hunks)
  • apps/web/test/utils/bookingScenario/bookingScenario.ts (1 hunks)
  • packages/features/bookings/Booker/components/DecoyBookingSuccessCard.tsx (1 hunks)
  • packages/features/bookings/Booker/components/hooks/useBookings.ts (7 hunks)
  • packages/features/bookings/Booker/components/hooks/useDecoyBooking.ts (1 hunks)
  • packages/features/bookings/components/BookingSuccessCard.tsx (1 hunks)
  • packages/features/bookings/lib/client/decoyBookingStore.ts (1 hunks)
  • packages/features/bookings/lib/handleNewBooking.ts (4 hunks)
  • packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts (1 hunks)
  • packages/features/bookings/lib/handleNewBooking/test/spam-booking.integration-test.ts (1 hunks)
  • packages/features/di/watchlist/containers/SpamCheckService.container.ts (1 hunks)
  • packages/features/di/watchlist/modules/Watchlist.module.ts (1 hunks)
  • packages/features/watchlist/lib/service/SpamCheckService.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/features/bookings/lib/client/decoyBookingStore.ts
  • apps/web/test/utils/bookingScenario/bookingScenario.ts
  • packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts
  • packages/features/bookings/Booker/components/hooks/useDecoyBooking.ts
  • packages/features/di/watchlist/containers/SpamCheckService.container.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.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/features/bookings/lib/handleNewBooking/test/spam-booking.integration-test.ts
  • packages/features/di/watchlist/modules/Watchlist.module.ts
  • packages/features/bookings/lib/handleNewBooking.ts
  • packages/features/watchlist/lib/service/SpamCheckService.ts
  • packages/features/bookings/Booker/components/hooks/useBookings.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/features/bookings/lib/handleNewBooking/test/spam-booking.integration-test.ts
  • packages/features/bookings/Booker/components/DecoyBookingSuccessCard.tsx
  • packages/features/di/watchlist/modules/Watchlist.module.ts
  • packages/features/bookings/components/BookingSuccessCard.tsx
  • apps/web/app/(booking-page-wrapper)/booking-successful/[uid]/page.tsx
  • packages/features/bookings/lib/handleNewBooking.ts
  • packages/features/watchlist/lib/service/SpamCheckService.ts
  • packages/features/bookings/Booker/components/hooks/useBookings.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/features/bookings/lib/handleNewBooking/test/spam-booking.integration-test.ts
  • packages/features/bookings/Booker/components/DecoyBookingSuccessCard.tsx
  • packages/features/di/watchlist/modules/Watchlist.module.ts
  • packages/features/bookings/components/BookingSuccessCard.tsx
  • apps/web/app/(booking-page-wrapper)/booking-successful/[uid]/page.tsx
  • packages/features/bookings/lib/handleNewBooking.ts
  • packages/features/watchlist/lib/service/SpamCheckService.ts
  • packages/features/bookings/Booker/components/hooks/useBookings.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/features/bookings/Booker/components/DecoyBookingSuccessCard.tsx
  • packages/features/bookings/components/BookingSuccessCard.tsx
  • apps/web/app/(booking-page-wrapper)/booking-successful/[uid]/page.tsx
**/*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/features/watchlist/lib/service/SpamCheckService.ts
🧠 Learnings (2)
📚 Learning: 2025-08-08T09:29:11.681Z
Learnt from: Udit-takkar
PR: calcom/cal.com#22919
File: packages/features/calAIPhone/interfaces/AIPhoneService.interface.ts:118-143
Timestamp: 2025-08-08T09:29:11.681Z
Learning: In calcom/cal.com PR #22919, packages/features/calAIPhone/interfaces/AIPhoneService.interface.ts (TypeScript), the AIPhoneServiceAgentListItem is required to include user.email in listAgents responses (per maintainer Udit-takkar). Future reviews should not flag this as unnecessary PII unless requirements change.

Applied to files:

  • apps/web/app/(booking-page-wrapper)/booking-successful/[uid]/page.tsx
📚 Learning: 2025-09-03T09:52:51.182Z
Learnt from: hariombalhara
PR: calcom/cal.com#23541
File: packages/features/bookings/lib/di/modules/RegularBookingServiceModule.ts:22-28
Timestamp: 2025-09-03T09:52:51.182Z
Learning: The IBookingServiceDependencies interface in packages/features/bookings/lib/handleNewBooking.ts contains 6 properties: cacheService, checkBookingAndDurationLimitsService, prismaClient, bookingRepository, featuresRepository, and checkBookingLimitsService. This interface is used by RegularBookingService and matches the depsMap structure in RegularBookingServiceModule.

Applied to files:

  • packages/features/bookings/lib/handleNewBooking.ts
🧬 Code graph analysis (6)
packages/features/bookings/lib/handleNewBooking/test/spam-booking.integration-test.ts (3)
packages/features/bookings/lib/handleNewBooking/test/getNewBookingHandler.ts (1)
  • getNewBookingHandler (15-17)
apps/web/test/utils/bookingScenario/bookingScenario.ts (9)
  • getBooker (2250-2264)
  • getOrganizer (1539-1598)
  • TestData (1258-1530)
  • getGoogleCalendarCredential (1211-1219)
  • createBookingScenario (997-1028)
  • getScenarioData (1600-1688)
  • mockCalendarToHaveNoBusySlots (1989-2002)
  • mockCalendarToCrashOnGetAvailability (2004-2006)
  • createOrganization (1040-1079)
packages/features/di/watchlist/containers/SpamCheckService.container.ts (1)
  • getSpamCheckService (7-11)
packages/features/di/watchlist/modules/Watchlist.module.ts (4)
packages/features/di/watchlist/Watchlist.tokens.ts (1)
  • WATCHLIST_DI_TOKENS (1-12)
packages/features/watchlist/lib/service/GlobalBlockingService.ts (1)
  • GlobalBlockingService (14-49)
packages/features/di/tokens.ts (1)
  • DI_TOKENS (5-62)
packages/features/watchlist/lib/service/OrganizationBlockingService.ts (1)
  • OrganizationBlockingService (14-59)
apps/web/app/(booking-page-wrapper)/booking-successful/[uid]/page.tsx (2)
packages/features/bookings/Booker/components/hooks/useDecoyBooking.ts (1)
  • useDecoyBooking (14-30)
packages/features/bookings/Booker/components/DecoyBookingSuccessCard.tsx (1)
  • DecoyBookingSuccessCard (18-114)
packages/features/bookings/lib/handleNewBooking.ts (1)
packages/features/di/watchlist/containers/SpamCheckService.container.ts (1)
  • getSpamCheckService (7-11)
packages/features/watchlist/lib/service/SpamCheckService.ts (3)
packages/features/watchlist/lib/interface/IBlockingService.ts (1)
  • BlockingResult (3-7)
packages/features/watchlist/lib/service/GlobalBlockingService.ts (1)
  • GlobalBlockingService (14-49)
packages/features/watchlist/lib/service/OrganizationBlockingService.ts (1)
  • OrganizationBlockingService (14-59)
packages/features/bookings/Booker/components/hooks/useBookings.ts (1)
packages/features/bookings/lib/client/decoyBookingStore.ts (1)
  • storeDecoyBooking (26-33)
🔇 Additional comments (4)
packages/features/bookings/Booker/components/hooks/useBookings.ts (2)

396-399: LGTM: safer error scrolling

Guarding scrollIntoView behind a ref check is good.


431-435: LGTM: consistent error UX for instant booking

Scroll into view on error is appropriate.

packages/features/bookings/lib/handleNewBooking.ts (1)

509-514: Good: start spam check early and run in parallel

Starting the check before heavy work reduces latency; passing orgId when available is right.

Please confirm SpamCheckService is fail-open on errors and timeouts so bookings aren’t blocked by infra issues.

packages/features/watchlist/lib/service/SpamCheckService.ts (1)

14-27: Overall structure looks good.

Naming, DI shape, and fail‑open catch in startCheck align with the design.

Ensure the DI container constructs a fresh SpamCheckService per request (no singleton). Based on container snippet, getSpamCheckService returns a new instance each call; confirm no global caching elsewhere.

Comment on lines +36 to +49
return (
<DecoyBookingSuccessCard
title={booking.title || "Booking"}
formattedDate={formattedDate}
formattedTime={formattedTime}
endTime={formattedEndTime}
formattedTimeZone={formattedTimeZone}
hostName={hostName}
hostEmail={hostEmail}
attendeeName={attendeeName}
attendeeEmail={attendeeEmail}
location={booking.location || null}
/>
);
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 | 🟠 Major

🧩 Analysis chain

Clear PII after render and localize fallback title

  • Remove decoy entry after reading to minimize PII retention.
  • Localize the fallback "Booking" string.
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { removeDecoyBooking } from "@calcom/features/bookings/lib/client/decoyBookingStore";
@@
 export default function BookingSuccessful() {
   const params = useParams();
+  const { t } = useLocale();
@@
   const bookingData = useDecoyBooking(uid);
@@
-    <DecoyBookingSuccessCard
-      title={booking.title || "Booking"}
+    {(() => {
+      // Clear decoy data after first render
+      removeDecoyBooking(uid);
+      return (
+        <DecoyBookingSuccessCard
+          title={booking.title || t("booking")}
           formattedDate={formattedDate}
           formattedTime={formattedTime}
           endTime={formattedEndTime}
           formattedTimeZone={formattedTimeZone}
           hostName={hostName}
           hostEmail={hostEmail}
           attendeeName={attendeeName}
           attendeeEmail={attendeeEmail}
           location={booking.location || null}
-    />
+        />
+      );
+    })()}

Run to verify TTL and removal behavior in the decoy store:


🏁 Script executed:

#!/bin/bash
# Inspect decoyBookingStore for TTL enforcement and removal
fd -t f 'decoyBookingStore.ts' | xargs -I{} sh -c 'echo "=== {} ==="; sed -n "1,200p" "{}"'
rg -n 'getDecoyBooking|removeDecoyBooking|timestamp|expiration|24' -C3

Length of output: 284658


🏁 Script executed:

#!/bin/bash
sed -n '1,240p' apps/web/app/\(booking-page-wrapper\)/booking-successful/\[uid\]/page.tsx || true

Length of output: 1759


Clear PII after render and localize fallback title

  • Replace the literal fallback "Booking" with localized text via useLocale (const { t } = useLocale(); title={booking.title || t("booking")}).
  • Delete stored decoy data after reading by calling removeDecoyBooking(uid) in a useEffect once bookingData is present (avoid performing removal during render).
  • File: apps/web/app/(booking-page-wrapper)/booking-successful/[uid]/page.tsx — update imports and add the useEffect around the existing render logic. Decoy store already enforces a 24h TTL and exposes removeDecoyBooking (verified).
🤖 Prompt for AI Agents
In apps/web/app/(booking-page-wrapper)/booking-successful/[uid]/page.tsx around
lines 36 to 49, the component uses a hardcoded fallback title and performs no
cleanup of stored decoy PII; update imports to include useLocale and useEffect
(and removeDecoyBooking from your decoy store), call const { t } = useLocale()
and replace title={booking.title || "Booking"} with title={booking.title ||
t("booking")}, and add a useEffect that, when bookingData (or booking) is
present, calls removeDecoyBooking(uid) once to delete stored decoy data (do not
call removal during render).

@devin-ai-integration devin-ai-integration bot force-pushed the devin/spam-async-check-1759840597 branch from 753737b to db1b5f4 Compare October 14, 2025 09:55
@hariombalhara hariombalhara force-pushed the devin/spam-async-check-1759840597 branch from db1b5f4 to c773994 Compare October 14, 2025 10:00
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 (4)
packages/features/bookings/components/BookingSuccessCard.tsx (1)

37-42: Accessibility blocker: aria-hidden hides dialog from screen readers

The container at line 37 has aria-hidden="true", which hides all its children (including the dialog with role="dialog" and aria-modal="true") from assistive technology.

Apply this fix:

-            <div className="main inset-0 my-4 flex flex-col transition-opacity sm:my-0" aria-hidden="true">
+            <div className="fixed inset-0 bg-black/50" aria-hidden="true" />
+            <div className="main inset-0 my-4 flex flex-col transition-opacity sm:my-0">
packages/features/bookings/lib/handleNewBooking/test/spam-booking.test.ts (1)

376-401: Mocking pattern won’t affect handler’s instance; stub container or prototype instead.

You spy on a fresh SpamCheckService instance, but handler() resolves its own via getSpamCheckService(); this spy won’t intercept it. Risk: false positives. Replace with module mock at top-level or prototype spy.

Apply this diff to remove the ineffective instance spy in this test body:

-        // Mock the SpamCheckService to throw an error during isBlocked check
-        const { getSpamCheckService } = await import(
-          "@calcom/features/di/watchlist/containers/SpamCheckService.container"
-        );
-        const spamCheckService = getSpamCheckService();
-        const originalIsBlocked = spamCheckService["isBlocked"].bind(spamCheckService);
-
-        // Use vi.spyOn to mock the private method
-        const isBlockedSpy = vi.spyOn(spamCheckService as never, "isBlocked");
-        isBlockedSpy.mockRejectedValue(new Error("Database connection failed"));
-
-        try {
+        {
           const createdBooking = await handleNewBooking({
             bookingData: mockBookingData,
           });
 
           // Should NOT be a decoy response - booking should succeed due to fail-open behavior
           expect(createdBooking).not.toHaveProperty("isShortCircuitedBooking");
           expect(createdBooking.status).toBe(BookingStatus.ACCEPTED);
           expect(createdBooking.id).not.toBe(0);
           expect(createdBooking.attendees[0].email).toBe(bookerEmail);
-        } finally {
-          // Restore original implementation and clean up spy
-          isBlockedSpy.mockRestore();
-          spamCheckService["isBlocked"] = originalIsBlocked;
         }

Add one of the following at the top of the file (outside tests) so it intercepts handler’s construction:

Option A — mock the container (preferred)

vi.mock("@calcom/features/di/watchlist/containers/SpamCheckService.container", () => {
  const mockService = {
    startCheck: vi.fn(),
    waitForCheck: vi.fn(async () => ({ isBlocked: false })),
    // Force fail-open path by rejecting isBlocked
    isBlocked: vi.fn().mockRejectedValue(new Error("Database connection failed")),
  };
  return { getSpamCheckService: () => mockService };
});

Option B — spy on the class prototype

import { SpamCheckService } from "@calcom/features/watchlist/lib/service/SpamCheckService";

const protoSpy = vi
  .spyOn(SpamCheckService.prototype as unknown as { isBlocked: (email: string, orgId?: number) => Promise<unknown> }, "isBlocked")
  .mockRejectedValue(new Error("Database connection failed"));

// ... run test and finally: protoSpy.mockRestore();
packages/features/bookings/lib/client/decoyBookingStore.ts (1)

59-69: Reduce TTL and consume-on-read to minimize PII exposure.

24h retention in localStorage is high risk; also clear data after a successful read.

-    // Check if the data is too old (24 hours)
+    // Check if the data is too old (30 minutes)
     const dataAge = Date.now() - data.timestamp;
-    const maxAge = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
+    const maxAge = 30 * 60 * 1000; // 30 minutes in milliseconds
@@
-    if (dataAge > maxAge) {
+    if (dataAge > maxAge) {
       // Remove the data from localStorage if expired
       localStorage.removeItem(storageKey);
       return null;
     }
 
-    return data;
+    // One-time consume on successful read
+    localStorage.removeItem(storageKey);
+    return data;

Optional: Prefer sessionStorage for even shorter persistence if UX allows.

packages/features/watchlist/lib/service/SpamCheckService.ts (1)

29-36: Enforce fail-open: add timeout and avoid throwing when not started.

Without a timeout, waitForCheck can hang and delay legit bookings; throwing if not started is brittle.

-  async waitForCheck(): Promise<BlockingResult> {
-    if (!this.spamCheckPromise) {
-      throw new Error(
-        "waitForCheck() called before startCheck(). You must call startCheck() first to initialize spam checking."
-      );
-    }
-    return await this.spamCheckPromise;
-  }
+  async waitForCheck(timeoutMs = 300): Promise<BlockingResult> {
+    if (!this.spamCheckPromise) return { isBlocked: false };
+    const timeout = new Promise<BlockingResult>((resolve) =>
+      setTimeout(() => resolve({ isBlocked: false }), timeoutMs)
+    );
+    return await Promise.race([this.spamCheckPromise, timeout]);
+  }

Note: getSpamCheckService returns a new instance per call, so no cross-request leakage. Based on learnings.

🧹 Nitpick comments (5)
packages/features/di/watchlist/containers/SpamCheckService.container.ts (1)

7-11: Remove redundant type casts.

Lines 8-9 cast to GlobalBlockingService and OrganizationBlockingService, but the getter functions already return these exact types (see watchlist.ts lines 41 and 48).

Apply this diff:

 export const getSpamCheckService = (): SpamCheckService => {
-  const globalBlockingService = getGlobalBlockingService() as GlobalBlockingService;
-  const organizationBlockingService = getOrganizationBlockingService() as OrganizationBlockingService;
+  const globalBlockingService = getGlobalBlockingService();
+  const organizationBlockingService = getOrganizationBlockingService();
   return new SpamCheckService(globalBlockingService, organizationBlockingService);
 };
apps/web/app/(booking-page-wrapper)/booking-successful/[localStorageUid]/page.tsx (1)

29-70: Consider localizing fallback and cleaning up PII.

Two recommended improvements:

  1. Localize the fallback title: Replace "Booking" at line 58 with t("booking") using useLocale().
  2. Remove decoy data after reading: Add a useEffect to call removeDecoyBooking(localStorageUid) once data is loaded, reducing PII retention.

The store already enforces 24h TTL, but explicit cleanup is a privacy best practice.

+"use client";
+
+import { useParams, useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+
+import dayjs from "@calcom/dayjs";
+import { DecoyBookingSuccessCard } from "@calcom/features/bookings/components/DecoyBookingSuccessCard";
+import { getDecoyBooking, removeDecoyBooking } from "@calcom/features/bookings/lib/client/decoyBookingStore";
+import type { DecoyBookingData } from "@calcom/features/bookings/lib/client/decoyBookingStore";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+
+function useDecoyBooking(localStorageUid: string) {
+  const router = useRouter();
+  const [bookingData, setBookingData] = useState<DecoyBookingData | null>(null);
+
+  useEffect(() => {
+    const data = getDecoyBooking(localStorageUid);
+
+    if (!data) {
+      router.push("/404");
+      return;
+    }
+
+    setBookingData(data);
+    // Clean up PII after reading
+    removeDecoyBooking(localStorageUid);
+  }, [localStorageUid, router]);
+
+  return bookingData;
+}
+
+export default function BookingSuccessful() {
+  const params = useParams();
+  const { t } = useLocale();
+
+  const localStorageUid = params?.localStorageUid as string;
+  const bookingData = useDecoyBooking(localStorageUid);
+
+  if (!bookingData) {
+    return null;
+  }
+
+  const { booking } = bookingData;
+
+  // ... rest of the component
+
+  return (
+    <DecoyBookingSuccessCard
+      title={booking.title || t("booking")}
+      // ... rest of props
+    />
+  );
+}
packages/features/bookings/lib/handleNewBooking/test/spam-booking.test.ts (2)

126-127: Remove debug logging from tests.

Drop console.log to keep CI output clean and avoid leaking data.

-        console.log("watchlists", await prismaMock.watchlist.findMany());

165-172: Assert no side effects for decoy bookings (no DB writes, no external calls).

Beyond findMany=0, assert booking.create wasn’t called to guarantee no writes; add similar checks for references, workflows, webhooks if applicable.

// after creating the booking and asserting decoy:
expect(prismaMock.booking.create).not.toHaveBeenCalled();
// Optional if these exist in prismaMock:
expect(prismaMock.bookingReference?.create).not.toHaveBeenCalled();
expect(prismaMock.workflowRun?.create).not.toHaveBeenCalled();
// And any email/webhook sender mocks if available.
packages/features/bookings/lib/client/decoyBookingStore.ts (1)

28-37: Store a sanitized shape; avoid persisting raw names/emails.

Write a minimal, redacted payload to storage instead of the full booking object.

-export function storeDecoyBooking(booking: Record<string, unknown>): string {
+export function storeDecoyBooking(booking: Record<string, unknown>): string {
   const localStorageUid = uuidv4();
-  const bookingSuccessData = {
-    booking,
+  // Build a minimal, sanitized snapshot for display only
+  const safeBooker = (booking as any)?.booker ?? null;
+  const redactedEmail =
+    safeBooker && typeof safeBooker.email === "string"
+      ? safeBooker.email.replace(/(^.).*(@.*$)/, "$1***$2")
+      : undefined;
+  const sanitized = {
+    title: (booking as any)?.title ?? null,
+    startTime: (booking as any)?.startTime,
+    endTime: (booking as any)?.endTime,
+    // Drop name; keep redacted email if present
+    booker: safeBooker ? { timeZone: safeBooker.timeZone, email: redactedEmail } : null,
+    host: (booking as any)?.host ? { name: (booking as any).host.name, timeZone: (booking as any).host.timeZone } : null,
+    location: (booking as any)?.location ?? null,
+  };
+  const bookingSuccessData: DecoyBookingData = {
+    booking: sanitized,
     timestamp: Date.now(),
   };
   const storageKey = getStorageKey(localStorageUid);
   localStorage.setItem(storageKey, JSON.stringify(bookingSuccessData));
   return localStorageUid;
}

If you prefer, we can make email optional in DecoyBookingData and omit it entirely.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • 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 bd229ca and db1b5f4.

📒 Files selected for processing (12)
  • apps/web/app/(booking-page-wrapper)/booking-successful/[localStorageUid]/page.tsx (1 hunks)
  • apps/web/test/utils/bookingScenario/bookingScenario.ts (2 hunks)
  • packages/features/bookings/Booker/components/hooks/useBookings.ts (7 hunks)
  • packages/features/bookings/components/BookingSuccessCard.tsx (1 hunks)
  • packages/features/bookings/components/DecoyBookingSuccessCard.tsx (1 hunks)
  • packages/features/bookings/lib/client/decoyBookingStore.ts (1 hunks)
  • packages/features/bookings/lib/handleNewBooking.ts (3 hunks)
  • packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts (2 hunks)
  • packages/features/bookings/lib/handleNewBooking/test/spam-booking.test.ts (1 hunks)
  • packages/features/di/watchlist/containers/SpamCheckService.container.ts (1 hunks)
  • packages/features/di/watchlist/containers/watchlist.ts (2 hunks)
  • packages/features/watchlist/lib/service/SpamCheckService.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/test/utils/bookingScenario/bookingScenario.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.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/features/di/watchlist/containers/SpamCheckService.container.ts
  • packages/features/di/watchlist/containers/watchlist.ts
  • packages/features/bookings/lib/handleNewBooking/test/spam-booking.test.ts
  • packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts
  • packages/features/watchlist/lib/service/SpamCheckService.ts
  • packages/features/bookings/lib/client/decoyBookingStore.ts
  • packages/features/bookings/lib/handleNewBooking.ts
  • packages/features/bookings/Booker/components/hooks/useBookings.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/features/di/watchlist/containers/SpamCheckService.container.ts
  • apps/web/app/(booking-page-wrapper)/booking-successful/[localStorageUid]/page.tsx
  • packages/features/di/watchlist/containers/watchlist.ts
  • packages/features/bookings/lib/handleNewBooking/test/spam-booking.test.ts
  • packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts
  • packages/features/watchlist/lib/service/SpamCheckService.ts
  • packages/features/bookings/components/DecoyBookingSuccessCard.tsx
  • packages/features/bookings/lib/client/decoyBookingStore.ts
  • packages/features/bookings/components/BookingSuccessCard.tsx
  • packages/features/bookings/lib/handleNewBooking.ts
  • packages/features/bookings/Booker/components/hooks/useBookings.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/features/di/watchlist/containers/SpamCheckService.container.ts
  • apps/web/app/(booking-page-wrapper)/booking-successful/[localStorageUid]/page.tsx
  • packages/features/di/watchlist/containers/watchlist.ts
  • packages/features/bookings/lib/handleNewBooking/test/spam-booking.test.ts
  • packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts
  • packages/features/watchlist/lib/service/SpamCheckService.ts
  • packages/features/bookings/components/DecoyBookingSuccessCard.tsx
  • packages/features/bookings/lib/client/decoyBookingStore.ts
  • packages/features/bookings/components/BookingSuccessCard.tsx
  • packages/features/bookings/lib/handleNewBooking.ts
  • packages/features/bookings/Booker/components/hooks/useBookings.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:

  • apps/web/app/(booking-page-wrapper)/booking-successful/[localStorageUid]/page.tsx
  • packages/features/bookings/components/DecoyBookingSuccessCard.tsx
  • packages/features/bookings/components/BookingSuccessCard.tsx
**/*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/features/watchlist/lib/service/SpamCheckService.ts
🧠 Learnings (6)
📚 Learning: 2025-08-08T09:29:11.681Z
Learnt from: Udit-takkar
PR: calcom/cal.com#22919
File: packages/features/calAIPhone/interfaces/AIPhoneService.interface.ts:118-143
Timestamp: 2025-08-08T09:29:11.681Z
Learning: In calcom/cal.com PR #22919, packages/features/calAIPhone/interfaces/AIPhoneService.interface.ts (TypeScript), the AIPhoneServiceAgentListItem is required to include user.email in listAgents responses (per maintainer Udit-takkar). Future reviews should not flag this as unnecessary PII unless requirements change.

Applied to files:

  • apps/web/app/(booking-page-wrapper)/booking-successful/[localStorageUid]/page.tsx
📚 Learning: 2025-08-21T13:44:06.805Z
Learnt from: supalarry
PR: calcom/cal.com#23217
File: apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts:93-94
Timestamp: 2025-08-21T13:44:06.805Z
Learning: In apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts, repository functions that use explicit Prisma select clauses (like getEventTypeWithSeats) are used for specific purposes and don't need to include all EventType fields like bookingRequiresAuthentication. These methods don't feed into the general OutputEventTypesService_2024_06_14 flow.

Applied to files:

  • packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts
📚 Learning: 2025-09-03T11:54:05.409Z
Learnt from: supalarry
PR: calcom/cal.com#23514
File: apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts:579-582
Timestamp: 2025-09-03T11:54:05.409Z
Learning: In calcom/cal.com bookings repository methods, when Prisma select uses `eventType: true`, all eventType fields including seatsShowAttendees are automatically included in the selection. Explicit field selection is not required when using `true` for nested relations.

Applied to files:

  • packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts
📚 Learning: 2025-09-03T11:54:05.409Z
Learnt from: supalarry
PR: calcom/cal.com#23514
File: apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts:579-582
Timestamp: 2025-09-03T11:54:05.409Z
Learning: In calcom/cal.com bookings repository methods, when Prisma include uses `eventType: true`, all eventType fields including seatsShowAttendees are automatically included in the selection. Explicit field selection is not required when using `true` for nested relations.

Applied to files:

  • packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts
📚 Learning: 2025-09-08T07:27:42.903Z
Learnt from: vijayraghav-io
PR: calcom/cal.com#16878
File: packages/app-store/feishucalendar/api/callback.ts:72-79
Timestamp: 2025-09-08T07:27:42.903Z
Learning: Four calendar integrations in Cal.com still use direct prisma.selectedCalendar.create instead of SelectedCalendarRepository.create: feishucalendar, zohocalendar, office365calendar, and larkcalendar. These bypass repository hooks and won't trigger reconnection logic for BookingReferences.

Applied to files:

  • packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts
📚 Learning: 2025-09-08T07:27:42.903Z
Learnt from: vijayraghav-io
PR: calcom/cal.com#16878
File: packages/app-store/feishucalendar/api/callback.ts:72-79
Timestamp: 2025-09-08T07:27:42.903Z
Learning: In the Cal.com codebase, some calendar integrations like google-calendar already use SelectedCalendarRepository.create for selectedCalendar creation, which automatically triggers reconnection logic, while others like feishucalendar use direct prisma.selectedCalendar.create calls that bypass the repository hooks.

Applied to files:

  • packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts
🧬 Code graph analysis (9)
packages/features/di/watchlist/containers/SpamCheckService.container.ts (2)
packages/features/watchlist/lib/service/SpamCheckService.ts (1)
  • SpamCheckService (14-57)
packages/features/di/watchlist/containers/watchlist.ts (2)
  • getGlobalBlockingService (41-46)
  • getOrganizationBlockingService (48-53)
apps/web/app/(booking-page-wrapper)/booking-successful/[localStorageUid]/page.tsx (2)
packages/features/bookings/lib/client/decoyBookingStore.ts (2)
  • DecoyBookingData (90-90)
  • getDecoyBooking (44-75)
packages/features/bookings/components/DecoyBookingSuccessCard.tsx (1)
  • DecoyBookingSuccessCard (18-114)
packages/features/di/watchlist/containers/watchlist.ts (6)
packages/features/watchlist/lib/service/GlobalBlockingService.ts (1)
  • GlobalBlockingService (14-49)
packages/features/watchlist/lib/interface/IWatchlistRepositories.ts (2)
  • IGlobalWatchlistRepository (9-36)
  • IOrganizationWatchlistRepository (42-79)
packages/features/di/watchlist/Watchlist.tokens.ts (1)
  • WATCHLIST_DI_TOKENS (1-12)
packages/features/watchlist/lib/service/OrganizationBlockingService.ts (1)
  • OrganizationBlockingService (14-59)
packages/features/watchlist/lib/service/WatchlistAuditService.ts (1)
  • WatchlistAuditService (13-44)
packages/features/watchlist/lib/interface/IAuditRepository.ts (1)
  • IAuditRepository (20-33)
packages/features/bookings/lib/handleNewBooking/test/spam-booking.test.ts (4)
packages/features/watchlist/lib/types.ts (1)
  • WatchlistType (3-3)
packages/features/bookings/lib/handleNewBooking/test/getNewBookingHandler.ts (1)
  • getNewBookingHandler (15-17)
apps/web/test/utils/bookingScenario/bookingScenario.ts (8)
  • getBooker (2250-2264)
  • getOrganizer (1539-1598)
  • TestData (1258-1530)
  • getGoogleCalendarCredential (1211-1219)
  • createBookingScenario (997-1028)
  • getScenarioData (1600-1688)
  • mockCalendarToHaveNoBusySlots (1989-2002)
  • mockCalendarToCrashOnGetAvailability (2012-2014)
packages/features/di/watchlist/containers/SpamCheckService.container.ts (1)
  • getSpamCheckService (7-11)
packages/features/watchlist/lib/service/SpamCheckService.ts (3)
packages/features/watchlist/lib/interface/IBlockingService.ts (1)
  • BlockingResult (3-7)
packages/features/watchlist/lib/service/GlobalBlockingService.ts (1)
  • GlobalBlockingService (14-49)
packages/features/watchlist/lib/service/OrganizationBlockingService.ts (1)
  • OrganizationBlockingService (14-59)
packages/features/bookings/lib/client/decoyBookingStore.ts (1)
packages/lib/webstorage.ts (1)
  • localStorage (6-36)
packages/features/bookings/components/BookingSuccessCard.tsx (1)
packages/ui/components/badge/index.ts (1)
  • Badge (1-1)
packages/features/bookings/lib/handleNewBooking.ts (1)
packages/features/di/watchlist/containers/SpamCheckService.container.ts (1)
  • getSpamCheckService (7-11)
packages/features/bookings/Booker/components/hooks/useBookings.ts (2)
packages/features/bookings/lib/client/decoyBookingStore.ts (1)
  • storeDecoyBooking (28-37)
packages/ui/components/toast/showToast.tsx (1)
  • showToast (77-101)
⏰ 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 (11)
packages/features/di/watchlist/containers/watchlist.ts (1)

41-46: LGTM! Facade pattern correctly implemented.

The getter now constructs a new service instance on each call rather than caching. This ensures fresh dependencies but creates a new instance per call.

If these services are called frequently, consider whether instance caching would be beneficial for performance. However, for the spam-check use case (called once per booking), fresh instantiation is acceptable.

packages/features/bookings/components/BookingSuccessCard.tsx (1)

1-114: Localization correctly implemented.

All user-facing strings use the t() function for proper internationalization.

packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts (1)

5-5: LGTM! Changes support spam check organization ID derivation.

The import reordering at line 5 is a cleanup, and the parentId addition at line 100 correctly supports the fallback pattern in handleNewBooking.ts where eventOrganizationId = team?.parentId ?? null.

Based on past review discussion confirming this field is consumed.

Also applies to: 100-100

packages/features/bookings/components/DecoyBookingSuccessCard.tsx (2)

37-42: Accessibility issue addressed in previous commit.

Per past review comments, the aria-hidden="true" issue on the dialog container was addressed in commits b01ceb4 to fe86a8c.


1-114: Localization correctly implemented.

All user-facing strings properly use t() for internationalization.

packages/features/bookings/lib/handleNewBooking.ts (3)

509-511: LGTM! Spam check correctly initiated early for parallel execution.

The spam check starts immediately after the blocker check, allowing it to run concurrently with availability checks and event processing. The eventOrganizationId derivation aligns with the parentId selection added in getEventTypesFromDB.ts.


1439-1445: Privacy consideration: attendee email included in decoy response.

The decoy includes the booker's email in the attendees array (line 1441), allowing them to see their own email in the success UI. This is stored in localStorage via the decoy store.

Per PR objectives, this is intentional - the attendee sees confirmation that their email was received. The host email remains hidden (line 1435 sets user.email: null).


1412-1451: Early return on spam-blocked path prevents downstream side effects
All side-effecting logic (createBooking, seat handling, eventManager calls, emails, webhooks, analytics) occurs after this guard and is skipped when spamCheckResult.isBlocked is true.

packages/features/bookings/Booker/components/hooks/useBookings.ts (3)

14-18: LGTM! Modular imports improve tree-shaking.

The switch from aggregated imports to individual function imports is a best practice for better bundle optimization.


281-294: Verify minimal PII is stored per past review guidance.

Past review comment (marked "✅ Addressed in commit aa82e15") suggested storing only minimal fields (title, times, names, emails, location) rather than full attendees[0] and user objects. However, lines 282-289 still store complete objects.

Please confirm whether:

  1. The fix was applied but not in this PR branch
  2. Storing full objects is now intentional
  3. The suggested minimal fields approach should still be implemented

If minimal PII storage is desired, apply the diff from the past review to store only necessary fields.


390-393: LGTM! Error handling improvements add safety.

The changes properly guard scrollIntoView with a null check (bookerFormErrorRef?.current) and remove unused error handler parameters.

Also applies to: 425-429

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/features/watchlist/lib/service/SpamCheckService.ts (1)

29-36: Add timeout and don’t throw when not started; enforce fail‑open

Currently, waitForCheck awaits indefinitely and throws if startCheck wasn’t called. Add a small timeout (e.g., 300ms) and return non‑blocked when not started.

Apply:

-  async waitForCheck(): Promise<BlockingResult> {
-    if (!this.spamCheckPromise) {
-      throw new Error(
-        "waitForCheck() called before startCheck(). You must call startCheck() first to initialize spam checking."
-      );
-    }
-    return await this.spamCheckPromise;
-  }
+  async waitForCheck(timeoutMs = 300): Promise<BlockingResult> {
+    if (!this.spamCheckPromise) return { isBlocked: false };
+    const timeout = new Promise<BlockingResult>((resolve) =>
+      setTimeout(() => resolve({ isBlocked: false }), timeoutMs)
+    );
+    return Promise.race([this.spamCheckPromise, timeout]);
+  }

This upholds “fail‑open” without delaying legitimate bookings. Based on learnings

🧹 Nitpick comments (5)
packages/features/bookings/lib/handleNewBooking.ts (4)

1414-1458: Short‑circuit earlier to avoid wasted work

You await spam check after loading workflows and doing heavy prep (availability, event building). If blocked, all that work is discarded. Move the await + decoy return to just before any side‑effects (DB/email/webhooks) and before expensive availability/users/workflows loading to save CPU/IO and reduce attack surface from spammers. Extract a small buildDecoyBooking() helper so you can reuse it in both places.


37-37: Prefer DI over container in handler

Resolve SpamCheckService via deps for testability and consistency with other services; avoid calling the container directly from the domain logic.

Minimal changes:

- import { getSpamCheckService } from "@calcom/features/di/watchlist/containers/SpamCheckService.container";
+ import type { SpamCheckService } from "@calcom/features/watchlist/lib/service/SpamCheckService";

Add to IBookingServiceDependencies (see separate comment), then:

- const spamCheckService = getSpamCheckService();
+ const { spamCheckService } = deps;
  // Either it is a team event or a managed child event of a managed event
  const team = eventType.team ?? eventType.parent?.team ?? null;
  const eventOrganizationId = team?.parentId ?? null;
  spamCheckService.startCheck({ email: bookerEmail, organizationId: eventOrganizationId });

Also applies to: 509-514


414-426: Wire SpamCheckService through deps

Add SpamCheckService to IBookingServiceDependencies so the handler doesn’t reach into the container.

Apply:

 export interface IBookingServiceDependencies {
   cacheService: CacheService;
   checkBookingAndDurationLimitsService: CheckBookingAndDurationLimitsService;
   prismaClient: PrismaClient;
   bookingRepository: BookingRepository;
   featuresRepository: FeaturesRepository;
   checkBookingLimitsService: CheckBookingLimitsService;
   luckyUserService: LuckyUserService;
   hostRepository: HostRepository;
   oooRepository: OooRepository;
   userRepository: UserRepository;
   attributeRepository: AttributeRepository;
+  spamCheckService: SpamCheckService;
 }

And import its type:

+import type { SpamCheckService } from "@calcom/features/watchlist/lib/service/SpamCheckService";

1414-1415: Fail‑open locally if spam wait throws

Guard against unexpected throws (e.g., future changes to waitForCheck). Default to non‑blocked.

-  const spamCheckResult = await spamCheckService.waitForCheck();
+  const spamCheckResult = await spamCheckService.waitForCheck().catch(() => ({ isBlocked: false }));
packages/features/watchlist/lib/service/SpamCheckService.ts (1)

45-47: Truthy check can skip organizationId=0

Guard explicitly for null/undefined to avoid accidental skips.

-    if (organizationId) {
+    if (organizationId !== null && organizationId !== undefined) {
       checks.push(this.organizationBlockingService.isBlocked(email, organizationId));
     }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • 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 bd229ca and a267c18.

📒 Files selected for processing (2)
  • packages/features/bookings/lib/handleNewBooking.ts (4 hunks)
  • packages/features/watchlist/lib/service/SpamCheckService.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/features/bookings/lib/handleNewBooking.ts
  • packages/features/watchlist/lib/service/SpamCheckService.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/features/bookings/lib/handleNewBooking.ts
  • packages/features/watchlist/lib/service/SpamCheckService.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/features/bookings/lib/handleNewBooking.ts
  • packages/features/watchlist/lib/service/SpamCheckService.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/features/watchlist/lib/service/SpamCheckService.ts
🧠 Learnings (1)
📚 Learning: 2025-09-03T09:52:51.182Z
Learnt from: hariombalhara
PR: calcom/cal.com#23541
File: packages/features/bookings/lib/di/modules/RegularBookingServiceModule.ts:22-28
Timestamp: 2025-09-03T09:52:51.182Z
Learning: The IBookingServiceDependencies interface in packages/features/bookings/lib/handleNewBooking.ts contains 6 properties: cacheService, checkBookingAndDurationLimitsService, prismaClient, bookingRepository, featuresRepository, and checkBookingLimitsService. This interface is used by RegularBookingService and matches the depsMap structure in RegularBookingServiceModule.

Applied to files:

  • packages/features/bookings/lib/handleNewBooking.ts
🧬 Code graph analysis (2)
packages/features/bookings/lib/handleNewBooking.ts (2)
packages/features/di/watchlist/containers/SpamCheckService.container.ts (1)
  • getSpamCheckService (7-11)
packages/features/eventtypes/lib/eventNaming.ts (1)
  • getEventName (31-113)
packages/features/watchlist/lib/service/SpamCheckService.ts (3)
packages/features/watchlist/lib/interface/IBlockingService.ts (1)
  • BlockingResult (3-7)
packages/features/watchlist/lib/service/GlobalBlockingService.ts (1)
  • GlobalBlockingService (14-49)
packages/features/watchlist/lib/service/OrganizationBlockingService.ts (1)
  • OrganizationBlockingService (14-59)

emrysal
emrysal previously approved these changes Oct 14, 2025
Copy link
Contributor

@emrysal emrysal left a comment

Choose a reason for hiding this comment

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

Happy for this decoy flow to go in.

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/features/watchlist/lib/service/SpamCheckService.ts (1)

29-36: Add timeout and fail‑open default to waitForCheck

Guarantee fail‑open even if dependencies stall; also avoid throwing when startCheck wasn’t called.

Apply:

-  async waitForCheck(): Promise<BlockingResult> {
-    if (!this.spamCheckPromise) {
-      throw new Error(
-        "waitForCheck() called before startCheck(). You must call startCheck() first to initialize spam checking."
-      );
-    }
-    return await this.spamCheckPromise;
-  }
+  async waitForCheck(timeoutMs = 300): Promise<BlockingResult> {
+    if (!this.spamCheckPromise) return { isBlocked: false };
+    const timeout = new Promise<BlockingResult>((resolve) =>
+      setTimeout(() => resolve({ isBlocked: false }), timeoutMs)
+    );
+    return Promise.race([this.spamCheckPromise, timeout]);
+  }
🧹 Nitpick comments (2)
packages/features/bookings/lib/handleNewBooking.ts (1)

509-514: Inject SpamCheckService via deps instead of importing container

Importing the container here couples domain logic to wiring and hinders testability. Prefer adding spamCheckService to IBookingServiceDependencies and pass it in from the module/container.

Based on learnings

packages/features/watchlist/lib/service/SpamCheckService.ts (1)

22-27: Tighten log message; keep semantics the same

Message better reflects runtime errors during the check.

-      logger.error("Error starting spam check", safeStringify(error));
+      logger.error("Spam check error", safeStringify(error));
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • 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 bd229ca and a267c18.

📒 Files selected for processing (2)
  • packages/features/bookings/lib/handleNewBooking.ts (4 hunks)
  • packages/features/watchlist/lib/service/SpamCheckService.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/features/watchlist/lib/service/SpamCheckService.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/features/watchlist/lib/service/SpamCheckService.ts
  • packages/features/bookings/lib/handleNewBooking.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/features/watchlist/lib/service/SpamCheckService.ts
  • packages/features/bookings/lib/handleNewBooking.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/features/watchlist/lib/service/SpamCheckService.ts
  • packages/features/bookings/lib/handleNewBooking.ts
🧠 Learnings (1)
📚 Learning: 2025-09-03T09:52:51.182Z
Learnt from: hariombalhara
PR: calcom/cal.com#23541
File: packages/features/bookings/lib/di/modules/RegularBookingServiceModule.ts:22-28
Timestamp: 2025-09-03T09:52:51.182Z
Learning: The IBookingServiceDependencies interface in packages/features/bookings/lib/handleNewBooking.ts contains 6 properties: cacheService, checkBookingAndDurationLimitsService, prismaClient, bookingRepository, featuresRepository, and checkBookingLimitsService. This interface is used by RegularBookingService and matches the depsMap structure in RegularBookingServiceModule.

Applied to files:

  • packages/features/bookings/lib/handleNewBooking.ts
🧬 Code graph analysis (2)
packages/features/watchlist/lib/service/SpamCheckService.ts (3)
packages/features/watchlist/lib/interface/IBlockingService.ts (1)
  • BlockingResult (3-7)
packages/features/watchlist/lib/service/GlobalBlockingService.ts (1)
  • GlobalBlockingService (14-49)
packages/features/watchlist/lib/service/OrganizationBlockingService.ts (1)
  • OrganizationBlockingService (14-59)
packages/features/bookings/lib/handleNewBooking.ts (2)
packages/features/di/watchlist/containers/SpamCheckService.container.ts (1)
  • getSpamCheckService (7-11)
packages/features/eventtypes/lib/eventNaming.ts (1)
  • getEventName (31-113)
🔇 Additional comments (1)
packages/features/watchlist/lib/service/SpamCheckService.ts (1)

42-62: Parallel checks with global precedence look good

Running global and org checks in parallel and preferring global results is sound.

@alishaz-polymath alishaz-polymath enabled auto-merge (squash) October 14, 2025 10:11
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: 2

♻️ Duplicate comments (8)
packages/features/bookings/lib/handleNewBooking/test/spam-booking.integration-test.ts (2)

13-13: Add missing semicolon

The import statement is missing a trailing semicolon for consistency with project formatting.

Apply this diff:

-import { prisma } from "@calcom/prisma"
+import { prisma } from "@calcom/prisma";

124-124: Remove debug console.log statement

This debug statement should be removed before merging to production.

Apply this diff:

-        console.log("watchlists", await prisma.watchlist.findMany());
-
packages/features/bookings/components/BookingSuccessCard.tsx (1)

37-42: Accessibility blocker: aria-hidden hides the dialog subtree

aria-hidden="true" on the container wrapping the dialog hides all its children from screen readers, including the dialog itself. This prevents screen reader users from accessing the booking confirmation content.

Apply this diff to fix the accessibility issue:

-            <div className="main inset-0 my-4 flex flex-col transition-opacity sm:my-0" aria-hidden="true">
+            {/* Overlay for background dimming */}
+            <div className="fixed inset-0 bg-black/50" aria-hidden="true" />
+            <div className="main inset-0 my-4 flex flex-col transition-opacity sm:my-0">

Based on coding guidelines: Always use t() for text localization in frontend code.

packages/features/bookings/Booker/components/DecoyBookingSuccessCard.tsx (1)

37-42: Accessibility blocker: aria-hidden still present despite past fix claim

The past review comment indicated this issue was addressed in commits b01ceb4 to fe86a8c, but aria-hidden="true" is still present on the container wrapping the dialog. This hides all children from screen readers, including the dialog itself.

Apply this diff:

-            <div className="main inset-0 my-4 flex flex-col transition-opacity sm:my-0" aria-hidden="true">
+            {/* Overlay for background dimming */}
+            <div className="fixed inset-0 bg-black/50" aria-hidden="true" />
+            <div className="main inset-0 my-4 flex flex-col transition-opacity sm:my-0">
apps/web/app/(booking-page-wrapper)/booking-successful/[uid]/page.tsx (1)

38-38: Localize the fallback title

The fallback string "Booking" should be localized using t() for consistency with the rest of the codebase and to support internationalization.

Apply this diff:

+import { useLocale } from "@calcom/lib/hooks/useLocale";
+
 export default function BookingSuccessful() {
   const params = useParams();
+  const { t } = useLocale();

   const uid = params?.uid as string;
   const bookingData = useDecoyBooking(uid);
   // ... rest of the code ...
   return (
     <DecoyBookingSuccessCard
-      title={booking.title || "Booking"}
+      title={booking.title || t("booking")}

As per coding guidelines: Always use t() for text localization in frontend code.

packages/features/bookings/Booker/components/hooks/useBookings.ts (1)

281-300: Minimize PII before persisting decoy booking to localStorage

You’re storing entire attendee and user objects. Persist only the minimal fields the decoy UI needs; sanitize the shape and type it to prevent accidental PII leakage.

Apply:

@@
-        const bookingData = {
-          uid: booking.uid,
-          title: booking.title ?? null,
-          startTime: booking.startTime,
-          endTime: booking.endTime,
-          booker: booking.attendees?.[0] ?? null,
-          host: booking.user ?? null,
-          location: booking.location ?? null,
-        };
-
-        storeDecoyBooking(bookingData);
+        const bookingData: DecoyBookingData["booking"] = {
+          uid: booking.uid,
+          title: booking.title ?? null,
+          startTime: booking.startTime,
+          endTime: booking.endTime,
+          booker: booking.attendees?.[0]
+            ? {
+                name: booking.attendees?.[0]?.name ?? "",
+                email: booking.attendees?.[0]?.email ?? "",
+                timeZone: booking.attendees?.[0]?.timeZone,
+              }
+            : null,
+          host: booking.user
+            ? {
+                name: booking.user?.name ?? "",
+                timeZone: booking.user?.timeZone,
+              }
+            : null,
+          location: booking.location ?? null,
+        };
+
+        storeDecoyBooking(bookingData);

Also add the type import near other imports:

+import type { DecoyBookingData } from "@calcom/features/bookings/lib/client/decoyBookingStore";
packages/features/bookings/lib/client/decoyBookingStore.ts (1)

55-66: Consume-on-read to further reduce PII exposure; confirm TTL aligns with product requirement

  • Remove the entry after a successful read to minimize persistence window.
  • TTL is 5 minutes; PR summary mentions 24 hours. Please confirm the intended TTL and adjust if needed.
-    // Check if the data is too old (5 min)
+    // Check if the data is too old (5 min)
     const dataAge = Date.now() - data.timestamp;
     const maxAge = 5 * 60 * 1000; // 5 minutes in milliseconds
 
     if (dataAge > maxAge) {
       // Remove the data from localStorage if expired
       localStorage.removeItem(storageKey);
       return null;
     }
 
-    return data;
+    // Consume-on-read: remove immediately to reduce PII exposure
+    localStorage.removeItem(storageKey);
+    return data;

If 24h is required, consider exporting a DEC0Y_BOOKING_TTL_MS constant and setting it centrally.

packages/features/watchlist/lib/service/SpamCheckService.ts (1)

29-36: Enforce fail‑open with a short timeout in waitForCheck

Avoid stalling booking creation if deps hang. Race the promise with a small timeout.

-  async waitForCheck(): Promise<BlockingResult> {
+  async waitForCheck(timeoutMs = 300): Promise<BlockingResult> {
     if (!this.spamCheckPromise) {
       throw new Error(
         "waitForCheck() called before startCheck(). You must call startCheck() first to initialize spam checking."
       );
     }
-    return await this.spamCheckPromise;
+    const timeout = new Promise<BlockingResult>((resolve) =>
+      setTimeout(() => resolve({ isBlocked: false }), timeoutMs)
+    );
+    return Promise.race([this.spamCheckPromise, timeout]);
   }
🧹 Nitpick comments (2)
packages/features/di/watchlist/modules/Watchlist.module.ts (1)

18-20: Minor formatting inconsistency

Missing space before the opening brace in the toClass call. Add a space for consistency with the binding above (line 14).

Apply this diff:

-watchlistModule.bind(WATCHLIST_DI_TOKENS.ORGANIZATION_BLOCKING_SERVICE).toClass(OrganizationBlockingService,{
+watchlistModule.bind(WATCHLIST_DI_TOKENS.ORGANIZATION_BLOCKING_SERVICE).toClass(OrganizationBlockingService, {
packages/features/bookings/Booker/components/DecoyBookingSuccessCard.tsx (1)

1-115: Consider consolidating duplicate success card components

DecoyBookingSuccessCard.tsx and BookingSuccessCard.tsx are nearly identical (same props interface, same rendering logic). Consider extracting a shared base component to reduce duplication and maintenance burden.

Example approach:

// BookingSuccessCardBase.tsx
export function BookingSuccessCardBase({ title, formattedDate, ... }: BookingSuccessCardProps) {
  // shared rendering logic
}

// DecoyBookingSuccessCard.tsx
export function DecoyBookingSuccessCard(props: DecoyBookingSuccessCardProps) {
  return <BookingSuccessCardBase {...props} />;
}

// BookingSuccessCard.tsx
export function BookingSuccessCard(props: BookingSuccessCardProps) {
  return <BookingSuccessCardBase {...props} />;
}
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • 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 db1b5f4 and a1dd9e5.

📒 Files selected for processing (13)
  • apps/web/app/(booking-page-wrapper)/booking-successful/[uid]/page.tsx (1 hunks)
  • apps/web/test/utils/bookingScenario/bookingScenario.ts (1 hunks)
  • packages/features/bookings/Booker/components/DecoyBookingSuccessCard.tsx (1 hunks)
  • packages/features/bookings/Booker/components/hooks/useBookings.ts (7 hunks)
  • packages/features/bookings/Booker/components/hooks/useDecoyBooking.ts (1 hunks)
  • packages/features/bookings/components/BookingSuccessCard.tsx (1 hunks)
  • packages/features/bookings/lib/client/decoyBookingStore.ts (1 hunks)
  • packages/features/bookings/lib/handleNewBooking.ts (4 hunks)
  • packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts (1 hunks)
  • packages/features/bookings/lib/handleNewBooking/test/spam-booking.integration-test.ts (1 hunks)
  • packages/features/di/watchlist/containers/SpamCheckService.container.ts (1 hunks)
  • packages/features/di/watchlist/modules/Watchlist.module.ts (1 hunks)
  • packages/features/watchlist/lib/service/SpamCheckService.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/features/di/watchlist/containers/SpamCheckService.container.ts
  • apps/web/test/utils/bookingScenario/bookingScenario.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*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/features/watchlist/lib/service/SpamCheckService.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/features/watchlist/lib/service/SpamCheckService.ts
  • packages/features/bookings/Booker/components/hooks/useBookings.ts
  • packages/features/bookings/lib/handleNewBooking/test/spam-booking.integration-test.ts
  • packages/features/bookings/lib/handleNewBooking.ts
  • packages/features/bookings/Booker/components/hooks/useDecoyBooking.ts
  • packages/features/bookings/lib/client/decoyBookingStore.ts
  • packages/features/di/watchlist/modules/Watchlist.module.ts
  • packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.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/features/watchlist/lib/service/SpamCheckService.ts
  • packages/features/bookings/Booker/components/hooks/useBookings.ts
  • packages/features/bookings/lib/handleNewBooking/test/spam-booking.integration-test.ts
  • packages/features/bookings/lib/handleNewBooking.ts
  • packages/features/bookings/Booker/components/hooks/useDecoyBooking.ts
  • packages/features/bookings/lib/client/decoyBookingStore.ts
  • packages/features/bookings/Booker/components/DecoyBookingSuccessCard.tsx
  • apps/web/app/(booking-page-wrapper)/booking-successful/[uid]/page.tsx
  • packages/features/di/watchlist/modules/Watchlist.module.ts
  • packages/features/bookings/components/BookingSuccessCard.tsx
  • packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.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/features/watchlist/lib/service/SpamCheckService.ts
  • packages/features/bookings/Booker/components/hooks/useBookings.ts
  • packages/features/bookings/lib/handleNewBooking/test/spam-booking.integration-test.ts
  • packages/features/bookings/lib/handleNewBooking.ts
  • packages/features/bookings/Booker/components/hooks/useDecoyBooking.ts
  • packages/features/bookings/lib/client/decoyBookingStore.ts
  • packages/features/bookings/Booker/components/DecoyBookingSuccessCard.tsx
  • apps/web/app/(booking-page-wrapper)/booking-successful/[uid]/page.tsx
  • packages/features/di/watchlist/modules/Watchlist.module.ts
  • packages/features/bookings/components/BookingSuccessCard.tsx
  • packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.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/features/bookings/Booker/components/DecoyBookingSuccessCard.tsx
  • apps/web/app/(booking-page-wrapper)/booking-successful/[uid]/page.tsx
  • packages/features/bookings/components/BookingSuccessCard.tsx
🧠 Learnings (5)
📚 Learning: 2025-09-03T09:52:51.182Z
Learnt from: hariombalhara
PR: calcom/cal.com#23541
File: packages/features/bookings/lib/di/modules/RegularBookingServiceModule.ts:22-28
Timestamp: 2025-09-03T09:52:51.182Z
Learning: The IBookingServiceDependencies interface in packages/features/bookings/lib/handleNewBooking.ts contains 6 properties: cacheService, checkBookingAndDurationLimitsService, prismaClient, bookingRepository, featuresRepository, and checkBookingLimitsService. This interface is used by RegularBookingService and matches the depsMap structure in RegularBookingServiceModule.

Applied to files:

  • packages/features/bookings/lib/handleNewBooking.ts
📚 Learning: 2025-08-08T09:29:11.681Z
Learnt from: Udit-takkar
PR: calcom/cal.com#22919
File: packages/features/calAIPhone/interfaces/AIPhoneService.interface.ts:118-143
Timestamp: 2025-08-08T09:29:11.681Z
Learning: In calcom/cal.com PR #22919, packages/features/calAIPhone/interfaces/AIPhoneService.interface.ts (TypeScript), the AIPhoneServiceAgentListItem is required to include user.email in listAgents responses (per maintainer Udit-takkar). Future reviews should not flag this as unnecessary PII unless requirements change.

Applied to files:

  • apps/web/app/(booking-page-wrapper)/booking-successful/[uid]/page.tsx
📚 Learning: 2025-08-21T13:44:06.805Z
Learnt from: supalarry
PR: calcom/cal.com#23217
File: apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts:93-94
Timestamp: 2025-08-21T13:44:06.805Z
Learning: In apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts, repository functions that use explicit Prisma select clauses (like getEventTypeWithSeats) are used for specific purposes and don't need to include all EventType fields like bookingRequiresAuthentication. These methods don't feed into the general OutputEventTypesService_2024_06_14 flow.

Applied to files:

  • packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts
📚 Learning: 2025-09-03T11:54:05.409Z
Learnt from: supalarry
PR: calcom/cal.com#23514
File: apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts:579-582
Timestamp: 2025-09-03T11:54:05.409Z
Learning: In calcom/cal.com bookings repository methods, when Prisma select uses `eventType: true`, all eventType fields including seatsShowAttendees are automatically included in the selection. Explicit field selection is not required when using `true` for nested relations.

Applied to files:

  • packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts
📚 Learning: 2025-09-03T11:54:05.409Z
Learnt from: supalarry
PR: calcom/cal.com#23514
File: apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts:579-582
Timestamp: 2025-09-03T11:54:05.409Z
Learning: In calcom/cal.com bookings repository methods, when Prisma include uses `eventType: true`, all eventType fields including seatsShowAttendees are automatically included in the selection. Explicit field selection is not required when using `true` for nested relations.

Applied to files:

  • packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts
🧬 Code graph analysis (8)
packages/features/watchlist/lib/service/SpamCheckService.ts (3)
packages/features/watchlist/lib/interface/IBlockingService.ts (1)
  • BlockingResult (3-7)
packages/features/watchlist/lib/service/GlobalBlockingService.ts (1)
  • GlobalBlockingService (14-49)
packages/features/watchlist/lib/service/OrganizationBlockingService.ts (1)
  • OrganizationBlockingService (14-59)
packages/features/bookings/Booker/components/hooks/useBookings.ts (2)
packages/features/bookings/lib/client/decoyBookingStore.ts (1)
  • storeDecoyBooking (26-33)
packages/ui/components/toast/showToast.tsx (1)
  • showToast (77-101)
packages/features/bookings/lib/handleNewBooking/test/spam-booking.integration-test.ts (2)
apps/web/test/utils/bookingScenario/bookingScenario.ts (9)
  • getBooker (2250-2264)
  • getOrganizer (1539-1598)
  • TestData (1258-1530)
  • getGoogleCalendarCredential (1211-1219)
  • createBookingScenario (997-1028)
  • getScenarioData (1600-1688)
  • mockCalendarToHaveNoBusySlots (1989-2002)
  • mockCalendarToCrashOnGetAvailability (2004-2006)
  • createOrganization (1040-1079)
packages/features/di/watchlist/containers/SpamCheckService.container.ts (1)
  • getSpamCheckService (7-11)
packages/features/bookings/lib/handleNewBooking.ts (2)
packages/features/di/watchlist/containers/SpamCheckService.container.ts (1)
  • getSpamCheckService (7-11)
packages/features/eventtypes/lib/eventNaming.ts (1)
  • getEventName (31-113)
packages/features/bookings/Booker/components/hooks/useDecoyBooking.ts (1)
packages/features/bookings/lib/client/decoyBookingStore.ts (2)
  • DecoyBookingData (86-86)
  • getDecoyBooking (40-71)
packages/features/bookings/lib/client/decoyBookingStore.ts (1)
packages/lib/webstorage.ts (1)
  • localStorage (6-36)
apps/web/app/(booking-page-wrapper)/booking-successful/[uid]/page.tsx (2)
packages/features/bookings/Booker/components/hooks/useDecoyBooking.ts (1)
  • useDecoyBooking (14-30)
packages/features/bookings/Booker/components/DecoyBookingSuccessCard.tsx (1)
  • DecoyBookingSuccessCard (18-114)
packages/features/di/watchlist/modules/Watchlist.module.ts (4)
packages/features/di/watchlist/Watchlist.tokens.ts (1)
  • WATCHLIST_DI_TOKENS (1-12)
packages/features/watchlist/lib/service/GlobalBlockingService.ts (1)
  • GlobalBlockingService (14-49)
packages/features/di/tokens.ts (1)
  • DI_TOKENS (5-62)
packages/features/watchlist/lib/service/OrganizationBlockingService.ts (1)
  • OrganizationBlockingService (14-59)
🔇 Additional comments (1)
packages/features/bookings/lib/handleNewBooking.ts (1)

1414-1458: Decoy branch skips all side effects
The early return in the blocked branch occurs before any DB writes, email/webhook dispatches, calendar syncs, payment handling, or workflow scheduling.

Copy link
Member

@alishaz-polymath alishaz-polymath left a comment

Choose a reason for hiding this comment

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

🎉

@github-actions
Copy link
Contributor

github-actions bot commented Oct 14, 2025

E2E results are ready!

@alishaz-polymath alishaz-polymath merged commit a0e5e03 into main Oct 14, 2025
59 of 62 checks passed
@alishaz-polymath alishaz-polymath deleted the devin/spam-async-check-1759840597 branch October 14, 2025 11:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bookings area: bookings, availability, timezones, double booking core area: core, team members only enterprise area: enterprise, audit log, organisation, SAML, SSO ✨ feature New feature or request ready-for-e2e size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants