Files
calendar/packages/features/bot-detection/BotDetectionService.ts
Benny JooGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
e3eaa69339 fix: return 400 instead of 500 for invalid eventTypeId in booking flow (#26732)
* fix: return 400 instead of 500 for invalid eventTypeId in booking flow

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* refactor: use ErrorWithCode instead of HttpError in BotDetectionService

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* refactor: use vi.stubEnv for safer environment variable handling in tests

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-12 07:07:35 -03:00

91 lines
2.7 KiB
TypeScript

import { checkBotId } from "botid/server";
import type { IncomingHttpHeaders } from "node:http";
import type { EventTypeRepository } from "@calcom/features/eventtypes/repositories/eventTypeRepository";
import type { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { ErrorWithCode } from "@calcom/lib/errors";
import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
interface BotDetectionConfig {
eventTypeId?: number;
headers: IncomingHttpHeaders;
}
const log = logger.getSubLogger({ prefix: ["[BotDetectionService]"] });
export class BotDetectionService {
constructor(
private featuresRepository: FeaturesRepository,
private eventTypeRepository: EventTypeRepository
) {}
private instanceHasBotIdEnabled() {
return process.env.NEXT_PUBLIC_VERCEL_USE_BOTID_IN_BOOKER === "1";
}
async checkBotDetection(config: BotDetectionConfig): Promise<void> {
if (!this.instanceHasBotIdEnabled()) return;
const { eventTypeId, headers } = config;
// If no eventTypeId provided, skip bot detection
if (!eventTypeId) {
return;
}
if (!Number.isInteger(eventTypeId) || eventTypeId <= 0) {
throw new ErrorWithCode(
ErrorCode.BadRequest,
`Invalid eventTypeId: ${eventTypeId}. Must be a positive integer.`
);
}
// Fetch only the teamId from the event type
const eventType = await this.eventTypeRepository.getTeamIdByEventTypeId({
id: eventTypeId,
});
// Only check for team events
if (!eventType?.teamId) {
return;
}
// Check if BotID feature is enabled for this team (also checks global scope - enabling on all teams)
const isBotIDEnabled = await this.featuresRepository.checkIfTeamHasFeature(
eventType.teamId,
"booker-botid"
);
if (!isBotIDEnabled) {
return;
}
// Perform bot detection
const verification = await checkBotId({
advancedOptions: {
headers,
},
});
// Log verification results with detailed information
const verificationDetails = {
isBot: verification.isBot,
isHuman: verification.isHuman,
isVerifiedBot: verification.isVerifiedBot,
verifiedBotName: verification.verifiedBotName,
verifiedBotCategory: verification.verifiedBotCategory,
bypassed: verification.bypassed,
classificationReason: verification.classificationReason,
teamId: eventType.teamId,
eventTypeId,
};
if (verification.isBot) {
log.warn("Bot detected - blocking request", verificationDetails);
throw new HttpError({ statusCode: 403, message: "Access denied" });
}
}
}