diff --git a/recipes/http-outgoingmessage-headers/codemod.yaml b/recipes/http-outgoingmessage-headers/codemod.yaml index 6cbdd472..02aac3d6 100644 --- a/recipes/http-outgoingmessage-headers/codemod.yaml +++ b/recipes/http-outgoingmessage-headers/codemod.yaml @@ -1,6 +1,6 @@ schema_version: "1.0" name: "@nodejs/http-outgoingmessage-headers" -version: 1.0.0 +version: 1.0.1 description: > Handle DEP0066: migrate deprecated use of `OutgoingMessage.prototype` `_headers` & `_headerNames` to public HTTP header APIs diff --git a/recipes/http-outgoingmessage-headers/tests/cal-com-fixutres/expected.ts b/recipes/http-outgoingmessage-headers/tests/cal-com-fixutres/expected.ts new file mode 100644 index 00000000..07b8ff6a --- /dev/null +++ b/recipes/http-outgoingmessage-headers/tests/cal-com-fixutres/expected.ts @@ -0,0 +1,91 @@ +// took form https://github.com/calcom/cal.diy/blob/e3eaa69339c549365f7749634e5d27f3aef1462f/packages/features/bot-detection/BotDetectionService.ts +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 { + 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" }); + } + } +} diff --git a/recipes/http-outgoingmessage-headers/tests/cal-com-fixutres/input.ts b/recipes/http-outgoingmessage-headers/tests/cal-com-fixutres/input.ts new file mode 100644 index 00000000..07b8ff6a --- /dev/null +++ b/recipes/http-outgoingmessage-headers/tests/cal-com-fixutres/input.ts @@ -0,0 +1,91 @@ +// took form https://github.com/calcom/cal.diy/blob/e3eaa69339c549365f7749634e5d27f3aef1462f/packages/features/bot-detection/BotDetectionService.ts +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 { + 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" }); + } + } +} diff --git a/recipes/mock-module-exports/codemod.yaml b/recipes/mock-module-exports/codemod.yaml index 6e37d3e1..56c69ca0 100644 --- a/recipes/mock-module-exports/codemod.yaml +++ b/recipes/mock-module-exports/codemod.yaml @@ -1,6 +1,6 @@ schema_version: "1.0" name: "@nodejs/mock-module-exports" -version: 1.0.0 +version: 1.0.1 description: "Handle mock.module exports deprecation" author: Bruno Rodrigues license: MIT diff --git a/recipes/mock-module-exports/tests/cal-com-fixture/expected.ts b/recipes/mock-module-exports/tests/cal-com-fixture/expected.ts new file mode 100644 index 00000000..29d0f0a3 --- /dev/null +++ b/recipes/mock-module-exports/tests/cal-com-fixture/expected.ts @@ -0,0 +1,112 @@ +// took form https://github.com/calcom/cal.diy/blob/e3eaa69339c549365f7749634e5d27f3aef1462f/packages/features/webhooks/lib/sendPayload.test.ts +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; + +import { WebhookVersion } from "./interface/IWebhookRepository"; +import sendPayload from "./sendPayload"; + +describe("sendPayload", () => { + const mockFetch = vi.fn(); + + beforeEach(() => { + vi.stubGlobal("fetch", mockFetch); + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + }); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + vi.resetAllMocks(); + }); + + describe("X-Cal-Webhook-Version header", () => { + it("should include X-Cal-Webhook-Version header with the webhook version", async () => { + const webhook = { + subscriberUrl: "https://example.com/webhook", + appId: null, + payloadTemplate: null, + version: WebhookVersion.V_2021_10_20, + }; + + await sendPayload("test-secret", "BOOKING_CREATED", new Date().toISOString(), webhook, { + title: "Test Booking", + startTime: "2024-01-01T10:00:00Z", + endTime: "2024-01-01T11:00:00Z", + organizer: { + email: "organizer@example.com", + name: "Organizer", + timeZone: "UTC", + language: { locale: "en" }, + }, + attendees: [], + type: "test-event", + description: "", + } as unknown as Parameters[4]); + + expect(mockFetch).toHaveBeenCalledTimes(1); + const [url, options] = mockFetch.mock.calls[0]; + + expect(url).toBe("https://example.com/webhook"); + expect(options.headers).toHaveProperty("X-Cal-Webhook-Version", "2021-10-20"); + }); + + it("should include X-Cal-Signature-256 header alongside version header", async () => { + const webhook = { + subscriberUrl: "https://example.com/webhook", + appId: null, + payloadTemplate: null, + version: WebhookVersion.V_2021_10_20, + }; + + await sendPayload("test-secret", "BOOKING_CREATED", new Date().toISOString(), webhook, { + title: "Test Booking", + startTime: "2024-01-01T10:00:00Z", + endTime: "2024-01-01T11:00:00Z", + organizer: { + email: "organizer@example.com", + name: "Organizer", + timeZone: "UTC", + language: { locale: "en" }, + }, + attendees: [], + type: "test-event", + description: "", + } as unknown as Parameters[4]); + + const [, options] = mockFetch.mock.calls[0]; + + expect(options.headers).toHaveProperty("X-Cal-Signature-256"); + expect(options.headers).toHaveProperty("X-Cal-Webhook-Version"); + expect(options.headers).toHaveProperty("Content-Type", "application/json"); + }); + + it("should send correct version for different webhook versions", async () => { + // Test with the current version + const webhook = { + subscriberUrl: "https://example.com/webhook", + appId: null, + payloadTemplate: null, + version: WebhookVersion.V_2021_10_20, + }; + + await sendPayload("test-secret", "BOOKING_CREATED", new Date().toISOString(), webhook, { + title: "Test", + startTime: "2024-01-01T10:00:00Z", + endTime: "2024-01-01T11:00:00Z", + organizer: { + email: "test@example.com", + name: "Test", + timeZone: "UTC", + language: { locale: "en" }, + }, + attendees: [], + type: "test", + description: "", + } as unknown as Parameters[4]); + + const [, options] = mockFetch.mock.calls[0]; + expect(options.headers["X-Cal-Webhook-Version"]).toBe("2021-10-20"); + }); + }); +}); diff --git a/recipes/mock-module-exports/tests/cal-com-fixture/input.ts b/recipes/mock-module-exports/tests/cal-com-fixture/input.ts new file mode 100644 index 00000000..29d0f0a3 --- /dev/null +++ b/recipes/mock-module-exports/tests/cal-com-fixture/input.ts @@ -0,0 +1,112 @@ +// took form https://github.com/calcom/cal.diy/blob/e3eaa69339c549365f7749634e5d27f3aef1462f/packages/features/webhooks/lib/sendPayload.test.ts +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; + +import { WebhookVersion } from "./interface/IWebhookRepository"; +import sendPayload from "./sendPayload"; + +describe("sendPayload", () => { + const mockFetch = vi.fn(); + + beforeEach(() => { + vi.stubGlobal("fetch", mockFetch); + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + }); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + vi.resetAllMocks(); + }); + + describe("X-Cal-Webhook-Version header", () => { + it("should include X-Cal-Webhook-Version header with the webhook version", async () => { + const webhook = { + subscriberUrl: "https://example.com/webhook", + appId: null, + payloadTemplate: null, + version: WebhookVersion.V_2021_10_20, + }; + + await sendPayload("test-secret", "BOOKING_CREATED", new Date().toISOString(), webhook, { + title: "Test Booking", + startTime: "2024-01-01T10:00:00Z", + endTime: "2024-01-01T11:00:00Z", + organizer: { + email: "organizer@example.com", + name: "Organizer", + timeZone: "UTC", + language: { locale: "en" }, + }, + attendees: [], + type: "test-event", + description: "", + } as unknown as Parameters[4]); + + expect(mockFetch).toHaveBeenCalledTimes(1); + const [url, options] = mockFetch.mock.calls[0]; + + expect(url).toBe("https://example.com/webhook"); + expect(options.headers).toHaveProperty("X-Cal-Webhook-Version", "2021-10-20"); + }); + + it("should include X-Cal-Signature-256 header alongside version header", async () => { + const webhook = { + subscriberUrl: "https://example.com/webhook", + appId: null, + payloadTemplate: null, + version: WebhookVersion.V_2021_10_20, + }; + + await sendPayload("test-secret", "BOOKING_CREATED", new Date().toISOString(), webhook, { + title: "Test Booking", + startTime: "2024-01-01T10:00:00Z", + endTime: "2024-01-01T11:00:00Z", + organizer: { + email: "organizer@example.com", + name: "Organizer", + timeZone: "UTC", + language: { locale: "en" }, + }, + attendees: [], + type: "test-event", + description: "", + } as unknown as Parameters[4]); + + const [, options] = mockFetch.mock.calls[0]; + + expect(options.headers).toHaveProperty("X-Cal-Signature-256"); + expect(options.headers).toHaveProperty("X-Cal-Webhook-Version"); + expect(options.headers).toHaveProperty("Content-Type", "application/json"); + }); + + it("should send correct version for different webhook versions", async () => { + // Test with the current version + const webhook = { + subscriberUrl: "https://example.com/webhook", + appId: null, + payloadTemplate: null, + version: WebhookVersion.V_2021_10_20, + }; + + await sendPayload("test-secret", "BOOKING_CREATED", new Date().toISOString(), webhook, { + title: "Test", + startTime: "2024-01-01T10:00:00Z", + endTime: "2024-01-01T11:00:00Z", + organizer: { + email: "test@example.com", + name: "Test", + timeZone: "UTC", + language: { locale: "en" }, + }, + attendees: [], + type: "test", + description: "", + } as unknown as Parameters[4]); + + const [, options] = mockFetch.mock.calls[0]; + expect(options.headers["X-Cal-Webhook-Version"]).toBe("2021-10-20"); + }); + }); +}); diff --git a/utils/src/ast-grep/import-statement.test.ts b/utils/src/ast-grep/import-statement.test.ts index 03853c92..0d7387a5 100644 --- a/utils/src/ast-grep/import-statement.test.ts +++ b/utils/src/ast-grep/import-statement.test.ts @@ -230,4 +230,40 @@ describe("import-statement", () => { assert.strictEqual(emptyImports.length, 1); assert.strictEqual(getDefaultImportIdentifier(emptyImports[0]), null); }); + + it("should ignore type imports", () => { + const code = dedent` + import fs from "fs"; + import type fsType from "fs"; + + import { join } from "node:path"; + import type { ParsedPath } from "node:path"; + + import type {} from "empty"; + `; + + const ast = astGrep.parse(astGrep.Lang.TypeScript, code); + + assert.strictEqual(getNodeImportStatements(ast, "fs").length, 1); + assert.strictEqual(getNodeImportStatements(ast, "path").length, 1); + assert.strictEqual(getNodeImportStatements(ast, "empty").length, 0); + }); + + it("should not partially match module names", () => { + const code = dedent` + import { describe } from "node:test"; + import { it } from "test"; + import { describe as vDescribe } from "vitest"; + import foo from "@scope/test"; + import bar from "test/utils"; + `; + + const ast = astGrep.parse(astGrep.Lang.JavaScript, code); + const imports = getNodeImportStatements(ast, "test"); + + assert.deepStrictEqual( + imports.map((i) => i.field("source")?.text()), + ['"node:test"', '"test"'], + ); + }); }); diff --git a/utils/src/ast-grep/import-statement.ts b/utils/src/ast-grep/import-statement.ts index 4d0336e6..e158936e 100644 --- a/utils/src/ast-grep/import-statement.ts +++ b/utils/src/ast-grep/import-statement.ts @@ -13,10 +13,14 @@ export const getNodeImportStatements = ( kind: 'string', has: { kind: 'string_fragment', - regex: `(node:)?${nodeModuleName}$`, + regex: `^(node:)?${nodeModuleName}$`, }, }, + not: { + pattern: 'import type $$$IMPORTS from $SOURCE', + } }, + }); /** @@ -62,7 +66,7 @@ export const getNodeImportCalls = ( kind: 'string', has: { kind: 'string_fragment', - regex: `(node:)?${nodeModuleName}$`, + regex: `^(node:)?${nodeModuleName}$`, }, }, }, @@ -84,7 +88,7 @@ export const getNodeImportCalls = ( kind: 'string', has: { kind: 'string_fragment', - regex: `(node:)?${nodeModuleName}$`, + regex: `^(node:)?${nodeModuleName}$`, }, }, },