Files
calendar/packages/features/watchlist/lib/service/SpamCheckService.ts
T
Hariom BalharaGitHubhariom@cal.com <hariombalhara@gmail.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
a0e5e03903 feat: Add async spam check and decoy booking response (#24326)
* feat: Add async spam check integration and decoy booking response

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

* Do checks in paralle

* Fix leaking host name in title

* Reduce expoiry time localstorage

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-14 11:36:51 +00:00

64 lines
2.2 KiB
TypeScript

import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import type { BlockingResult } from "../interface/IBlockingService";
import type { GlobalBlockingService } from "./GlobalBlockingService";
import type { OrganizationBlockingService } from "./OrganizationBlockingService";
/**
* Spam Check Service - Orchestrates spam checking by coordinating blocking checks
*
* Checks both global watchlist entries (via GlobalBlockingService) and organization-specific
* watchlist entries (via OrganizationBlockingService) when an organizationId is provided.
*/
export class SpamCheckService {
private spamCheckPromise: Promise<BlockingResult> | null = null;
constructor(
private readonly globalBlockingService: GlobalBlockingService,
private readonly organizationBlockingService: OrganizationBlockingService
) { }
startCheck({ email, organizationId }: { email: string, organizationId: number | null }): void {
this.spamCheckPromise = this.isBlocked(email, organizationId ?? undefined).catch((error) => {
logger.error("Error starting spam check", safeStringify(error));
return { isBlocked: false };
});
}
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;
}
/**
* Checks if an email is blocked by global or organization-specific watchlist rules
* Runs both checks in parallel for better performance
*/
private async isBlocked(email: string, organizationId?: number): Promise<BlockingResult> {
const checks = [this.globalBlockingService.isBlocked(email)];
if (organizationId) {
checks.push(this.organizationBlockingService.isBlocked(email, organizationId));
}
const [globalResult, orgResult] = await Promise.all(checks);
// Global blocking takes precedence
if (globalResult.isBlocked) {
return globalResult;
}
// Check organization blocking if it was performed
if (orgResult?.isBlocked) {
return orgResult;
}
return { isBlocked: false };
}
}