* 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>
64 lines
2.2 KiB
TypeScript
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 };
|
|
}
|
|
}
|