From 0e7840bd8a134714e53b7dc20246e382093b8400 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Mon, 13 Jul 2026 22:55:52 +0200 Subject: [PATCH 1/4] fix(http-outgoingmessage-headers): crash cause: .slip on undefined dued to catching imported type --- .../tests/cal-com-fixutres/expected.ts | 91 +++++++++++++++++++ .../tests/cal-com-fixutres/input.ts | 91 +++++++++++++++++++ .../{expected.cjs => expected.ts} | 2 +- .../check-if-exists/{input.cjs => input.ts} | 2 +- .../{expected.cjs => expected.ts} | 2 +- .../{input.cjs => input.ts} | 2 +- .../{expected.cjs => expected.ts} | 2 +- .../reading-headers/{input.cjs => input.ts} | 2 +- .../{expected.cjs => expected.ts} | 2 +- .../supported-methods/{input.cjs => input.ts} | 2 +- utils/src/ast-grep/import-statement.test.ts | 18 ++++ utils/src/ast-grep/import-statement.ts | 9 +- 12 files changed, 215 insertions(+), 10 deletions(-) create mode 100644 recipes/http-outgoingmessage-headers/tests/cal-com-fixutres/expected.ts create mode 100644 recipes/http-outgoingmessage-headers/tests/cal-com-fixutres/input.ts rename recipes/http-outgoingmessage-headers/tests/check-if-exists/{expected.cjs => expected.ts} (95%) rename recipes/http-outgoingmessage-headers/tests/check-if-exists/{input.cjs => input.ts} (95%) rename recipes/http-outgoingmessage-headers/tests/iterating-over-headers/{expected.cjs => expected.ts} (97%) rename recipes/http-outgoingmessage-headers/tests/iterating-over-headers/{input.cjs => input.ts} (97%) rename recipes/http-outgoingmessage-headers/tests/reading-headers/{expected.cjs => expected.ts} (92%) rename recipes/http-outgoingmessage-headers/tests/reading-headers/{input.cjs => input.ts} (92%) rename recipes/http-outgoingmessage-headers/tests/supported-methods/{expected.cjs => expected.ts} (94%) rename recipes/http-outgoingmessage-headers/tests/supported-methods/{input.cjs => input.ts} (93%) 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/http-outgoingmessage-headers/tests/check-if-exists/expected.cjs b/recipes/http-outgoingmessage-headers/tests/check-if-exists/expected.ts similarity index 95% rename from recipes/http-outgoingmessage-headers/tests/check-if-exists/expected.cjs rename to recipes/http-outgoingmessage-headers/tests/check-if-exists/expected.ts index 5b35abee..d10c86e1 100644 --- a/recipes/http-outgoingmessage-headers/tests/check-if-exists/expected.cjs +++ b/recipes/http-outgoingmessage-headers/tests/check-if-exists/expected.ts @@ -1,4 +1,4 @@ -const http = require('http'); +const http = require('node:http'); const server = http.createServer((req, res) => { res.setHeader('content-type', 'application/json'); diff --git a/recipes/http-outgoingmessage-headers/tests/check-if-exists/input.cjs b/recipes/http-outgoingmessage-headers/tests/check-if-exists/input.ts similarity index 95% rename from recipes/http-outgoingmessage-headers/tests/check-if-exists/input.cjs rename to recipes/http-outgoingmessage-headers/tests/check-if-exists/input.ts index 1945c0ec..ea030cb3 100644 --- a/recipes/http-outgoingmessage-headers/tests/check-if-exists/input.cjs +++ b/recipes/http-outgoingmessage-headers/tests/check-if-exists/input.ts @@ -1,4 +1,4 @@ -const http = require('http'); +const http = require('node:http'); const server = http.createServer((req, res) => { res.setHeader('content-type', 'application/json'); diff --git a/recipes/http-outgoingmessage-headers/tests/iterating-over-headers/expected.cjs b/recipes/http-outgoingmessage-headers/tests/iterating-over-headers/expected.ts similarity index 97% rename from recipes/http-outgoingmessage-headers/tests/iterating-over-headers/expected.cjs rename to recipes/http-outgoingmessage-headers/tests/iterating-over-headers/expected.ts index 2a63ca53..2a913423 100644 --- a/recipes/http-outgoingmessage-headers/tests/iterating-over-headers/expected.cjs +++ b/recipes/http-outgoingmessage-headers/tests/iterating-over-headers/expected.ts @@ -1,4 +1,4 @@ -const http = require('http'); +const http = require('node:http'); const server = http.createServer((req, res) => { res.setHeader('content-type', 'application/json'); diff --git a/recipes/http-outgoingmessage-headers/tests/iterating-over-headers/input.cjs b/recipes/http-outgoingmessage-headers/tests/iterating-over-headers/input.ts similarity index 97% rename from recipes/http-outgoingmessage-headers/tests/iterating-over-headers/input.cjs rename to recipes/http-outgoingmessage-headers/tests/iterating-over-headers/input.ts index 0a50181e..f98373ea 100644 --- a/recipes/http-outgoingmessage-headers/tests/iterating-over-headers/input.cjs +++ b/recipes/http-outgoingmessage-headers/tests/iterating-over-headers/input.ts @@ -1,4 +1,4 @@ -const http = require('http'); +const http = require('node:http'); const server = http.createServer((req, res) => { res.setHeader('content-type', 'application/json'); diff --git a/recipes/http-outgoingmessage-headers/tests/reading-headers/expected.cjs b/recipes/http-outgoingmessage-headers/tests/reading-headers/expected.ts similarity index 92% rename from recipes/http-outgoingmessage-headers/tests/reading-headers/expected.cjs rename to recipes/http-outgoingmessage-headers/tests/reading-headers/expected.ts index 59028117..89982e86 100644 --- a/recipes/http-outgoingmessage-headers/tests/reading-headers/expected.cjs +++ b/recipes/http-outgoingmessage-headers/tests/reading-headers/expected.ts @@ -1,4 +1,4 @@ -const http = require('http'); +const http = require('node:http'); function handler(req, res) { res.setHeader('content-type', 'application/json'); diff --git a/recipes/http-outgoingmessage-headers/tests/reading-headers/input.cjs b/recipes/http-outgoingmessage-headers/tests/reading-headers/input.ts similarity index 92% rename from recipes/http-outgoingmessage-headers/tests/reading-headers/input.cjs rename to recipes/http-outgoingmessage-headers/tests/reading-headers/input.ts index 1043cda1..50a31f16 100644 --- a/recipes/http-outgoingmessage-headers/tests/reading-headers/input.cjs +++ b/recipes/http-outgoingmessage-headers/tests/reading-headers/input.ts @@ -1,4 +1,4 @@ -const http = require('http'); +const http = require('node:http'); function handler(req, res) { res.setHeader('content-type', 'application/json'); diff --git a/recipes/http-outgoingmessage-headers/tests/supported-methods/expected.cjs b/recipes/http-outgoingmessage-headers/tests/supported-methods/expected.ts similarity index 94% rename from recipes/http-outgoingmessage-headers/tests/supported-methods/expected.cjs rename to recipes/http-outgoingmessage-headers/tests/supported-methods/expected.ts index 59d9871e..6a06b1c4 100644 --- a/recipes/http-outgoingmessage-headers/tests/supported-methods/expected.cjs +++ b/recipes/http-outgoingmessage-headers/tests/supported-methods/expected.ts @@ -1,4 +1,4 @@ -const http = require('http'); +const http = require('node:http'); const server = http.createServer((req, res) => { res.setHeader('content-type', 'application/json'); diff --git a/recipes/http-outgoingmessage-headers/tests/supported-methods/input.cjs b/recipes/http-outgoingmessage-headers/tests/supported-methods/input.ts similarity index 93% rename from recipes/http-outgoingmessage-headers/tests/supported-methods/input.cjs rename to recipes/http-outgoingmessage-headers/tests/supported-methods/input.ts index bb4a8804..5030e312 100644 --- a/recipes/http-outgoingmessage-headers/tests/supported-methods/input.cjs +++ b/recipes/http-outgoingmessage-headers/tests/supported-methods/input.ts @@ -1,4 +1,4 @@ -const http = require('http'); +const http = require('node:http'); const server = http.createServer((req, res) => { res.setHeader('content-type', 'application/json'); diff --git a/utils/src/ast-grep/import-statement.test.ts b/utils/src/ast-grep/import-statement.test.ts index 03853c92..7b44bba3 100644 --- a/utils/src/ast-grep/import-statement.test.ts +++ b/utils/src/ast-grep/import-statement.test.ts @@ -230,4 +230,22 @@ 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); + }); }); diff --git a/utils/src/ast-grep/import-statement.ts b/utils/src/ast-grep/import-statement.ts index 4d0336e6..c733287b 100644 --- a/utils/src/ast-grep/import-statement.ts +++ b/utils/src/ast-grep/import-statement.ts @@ -1,10 +1,11 @@ import type { Rule, SgNode, SgRoot } from '@codemod.com/jssg-types/main'; import type Js from '@codemod.com/jssg-types/langs/javascript'; +import type Ts from '@codemod.com/jssg-types/langs/typescript'; export const getNodeImportStatements = ( - rootNode: SgRoot, + rootNode: SgRoot, nodeModuleName: string, -): SgNode[] => +): SgNode[] => rootNode.root().findAll({ rule: { kind: 'import_statement', @@ -16,7 +17,11 @@ export const getNodeImportStatements = ( regex: `(node:)?${nodeModuleName}$`, }, }, + not: { + pattern: 'import type $$$IMPORTS from $SOURCE', + } }, + }); /** From f90daf7cecb1475c69b9fe28f855fa1e6073f202 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Mon, 13 Jul 2026 23:07:33 +0200 Subject: [PATCH 2/4] fix(mock-module-exports): crash cause: import-statement was catching `vitest` dude to `vite` --- .../tests/cal-com-fixture/expected.ts | 112 ++++++++++++++++++ .../tests/cal-com-fixture/input.ts | 112 ++++++++++++++++++ utils/src/ast-grep/import-statement.test.ts | 20 ++++ utils/src/ast-grep/import-statement.ts | 6 +- 4 files changed, 247 insertions(+), 3 deletions(-) create mode 100644 recipes/mock-module-exports/tests/cal-com-fixture/expected.ts create mode 100644 recipes/mock-module-exports/tests/cal-com-fixture/input.ts 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 7b44bba3..a3e77abb 100644 --- a/utils/src/ast-grep/import-statement.test.ts +++ b/utils/src/ast-grep/import-statement.test.ts @@ -248,4 +248,24 @@ describe("import-statement", () => { 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.strictEqual(imports.length, 2); + 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 c733287b..5c23ebbd 100644 --- a/utils/src/ast-grep/import-statement.ts +++ b/utils/src/ast-grep/import-statement.ts @@ -14,7 +14,7 @@ export const getNodeImportStatements = ( kind: 'string', has: { kind: 'string_fragment', - regex: `(node:)?${nodeModuleName}$`, + regex: `^(node:)?${nodeModuleName}$`, }, }, not: { @@ -67,7 +67,7 @@ export const getNodeImportCalls = ( kind: 'string', has: { kind: 'string_fragment', - regex: `(node:)?${nodeModuleName}$`, + regex: `^(node:)?${nodeModuleName}$`, }, }, }, @@ -89,7 +89,7 @@ export const getNodeImportCalls = ( kind: 'string', has: { kind: 'string_fragment', - regex: `(node:)?${nodeModuleName}$`, + regex: `^(node:)?${nodeModuleName}$`, }, }, }, From 39e02c02089d27a99b37f86a3e7da2cbab3f299c Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Mon, 13 Jul 2026 23:09:49 +0200 Subject: [PATCH 3/4] fix: type-check --- utils/src/ast-grep/import-statement.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/utils/src/ast-grep/import-statement.ts b/utils/src/ast-grep/import-statement.ts index 5c23ebbd..e158936e 100644 --- a/utils/src/ast-grep/import-statement.ts +++ b/utils/src/ast-grep/import-statement.ts @@ -1,11 +1,10 @@ import type { Rule, SgNode, SgRoot } from '@codemod.com/jssg-types/main'; import type Js from '@codemod.com/jssg-types/langs/javascript'; -import type Ts from '@codemod.com/jssg-types/langs/typescript'; export const getNodeImportStatements = ( - rootNode: SgRoot, + rootNode: SgRoot, nodeModuleName: string, -): SgNode[] => +): SgNode[] => rootNode.root().findAll({ rule: { kind: 'import_statement', From a7d942506236a2439ebdd72848e32ad0a8eca197 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Tue, 14 Jul 2026 22:50:43 +0200 Subject: [PATCH 4/4] apply suggestion --- .../tests/check-if-exists/{expected.ts => expected.cjs} | 2 +- .../tests/check-if-exists/{input.ts => input.cjs} | 2 +- .../tests/iterating-over-headers/{expected.ts => expected.cjs} | 2 +- .../tests/iterating-over-headers/{input.ts => input.cjs} | 2 +- .../tests/reading-headers/{expected.ts => expected.cjs} | 2 +- .../tests/reading-headers/{input.ts => input.cjs} | 2 +- .../tests/supported-methods/{expected.ts => expected.cjs} | 2 +- .../tests/supported-methods/{input.ts => input.cjs} | 2 +- utils/src/ast-grep/import-statement.test.ts | 2 -- 9 files changed, 8 insertions(+), 10 deletions(-) rename recipes/http-outgoingmessage-headers/tests/check-if-exists/{expected.ts => expected.cjs} (95%) rename recipes/http-outgoingmessage-headers/tests/check-if-exists/{input.ts => input.cjs} (95%) rename recipes/http-outgoingmessage-headers/tests/iterating-over-headers/{expected.ts => expected.cjs} (97%) rename recipes/http-outgoingmessage-headers/tests/iterating-over-headers/{input.ts => input.cjs} (97%) rename recipes/http-outgoingmessage-headers/tests/reading-headers/{expected.ts => expected.cjs} (92%) rename recipes/http-outgoingmessage-headers/tests/reading-headers/{input.ts => input.cjs} (92%) rename recipes/http-outgoingmessage-headers/tests/supported-methods/{expected.ts => expected.cjs} (94%) rename recipes/http-outgoingmessage-headers/tests/supported-methods/{input.ts => input.cjs} (93%) diff --git a/recipes/http-outgoingmessage-headers/tests/check-if-exists/expected.ts b/recipes/http-outgoingmessage-headers/tests/check-if-exists/expected.cjs similarity index 95% rename from recipes/http-outgoingmessage-headers/tests/check-if-exists/expected.ts rename to recipes/http-outgoingmessage-headers/tests/check-if-exists/expected.cjs index d10c86e1..5b35abee 100644 --- a/recipes/http-outgoingmessage-headers/tests/check-if-exists/expected.ts +++ b/recipes/http-outgoingmessage-headers/tests/check-if-exists/expected.cjs @@ -1,4 +1,4 @@ -const http = require('node:http'); +const http = require('http'); const server = http.createServer((req, res) => { res.setHeader('content-type', 'application/json'); diff --git a/recipes/http-outgoingmessage-headers/tests/check-if-exists/input.ts b/recipes/http-outgoingmessage-headers/tests/check-if-exists/input.cjs similarity index 95% rename from recipes/http-outgoingmessage-headers/tests/check-if-exists/input.ts rename to recipes/http-outgoingmessage-headers/tests/check-if-exists/input.cjs index ea030cb3..1945c0ec 100644 --- a/recipes/http-outgoingmessage-headers/tests/check-if-exists/input.ts +++ b/recipes/http-outgoingmessage-headers/tests/check-if-exists/input.cjs @@ -1,4 +1,4 @@ -const http = require('node:http'); +const http = require('http'); const server = http.createServer((req, res) => { res.setHeader('content-type', 'application/json'); diff --git a/recipes/http-outgoingmessage-headers/tests/iterating-over-headers/expected.ts b/recipes/http-outgoingmessage-headers/tests/iterating-over-headers/expected.cjs similarity index 97% rename from recipes/http-outgoingmessage-headers/tests/iterating-over-headers/expected.ts rename to recipes/http-outgoingmessage-headers/tests/iterating-over-headers/expected.cjs index 2a913423..2a63ca53 100644 --- a/recipes/http-outgoingmessage-headers/tests/iterating-over-headers/expected.ts +++ b/recipes/http-outgoingmessage-headers/tests/iterating-over-headers/expected.cjs @@ -1,4 +1,4 @@ -const http = require('node:http'); +const http = require('http'); const server = http.createServer((req, res) => { res.setHeader('content-type', 'application/json'); diff --git a/recipes/http-outgoingmessage-headers/tests/iterating-over-headers/input.ts b/recipes/http-outgoingmessage-headers/tests/iterating-over-headers/input.cjs similarity index 97% rename from recipes/http-outgoingmessage-headers/tests/iterating-over-headers/input.ts rename to recipes/http-outgoingmessage-headers/tests/iterating-over-headers/input.cjs index f98373ea..0a50181e 100644 --- a/recipes/http-outgoingmessage-headers/tests/iterating-over-headers/input.ts +++ b/recipes/http-outgoingmessage-headers/tests/iterating-over-headers/input.cjs @@ -1,4 +1,4 @@ -const http = require('node:http'); +const http = require('http'); const server = http.createServer((req, res) => { res.setHeader('content-type', 'application/json'); diff --git a/recipes/http-outgoingmessage-headers/tests/reading-headers/expected.ts b/recipes/http-outgoingmessage-headers/tests/reading-headers/expected.cjs similarity index 92% rename from recipes/http-outgoingmessage-headers/tests/reading-headers/expected.ts rename to recipes/http-outgoingmessage-headers/tests/reading-headers/expected.cjs index 89982e86..59028117 100644 --- a/recipes/http-outgoingmessage-headers/tests/reading-headers/expected.ts +++ b/recipes/http-outgoingmessage-headers/tests/reading-headers/expected.cjs @@ -1,4 +1,4 @@ -const http = require('node:http'); +const http = require('http'); function handler(req, res) { res.setHeader('content-type', 'application/json'); diff --git a/recipes/http-outgoingmessage-headers/tests/reading-headers/input.ts b/recipes/http-outgoingmessage-headers/tests/reading-headers/input.cjs similarity index 92% rename from recipes/http-outgoingmessage-headers/tests/reading-headers/input.ts rename to recipes/http-outgoingmessage-headers/tests/reading-headers/input.cjs index 50a31f16..1043cda1 100644 --- a/recipes/http-outgoingmessage-headers/tests/reading-headers/input.ts +++ b/recipes/http-outgoingmessage-headers/tests/reading-headers/input.cjs @@ -1,4 +1,4 @@ -const http = require('node:http'); +const http = require('http'); function handler(req, res) { res.setHeader('content-type', 'application/json'); diff --git a/recipes/http-outgoingmessage-headers/tests/supported-methods/expected.ts b/recipes/http-outgoingmessage-headers/tests/supported-methods/expected.cjs similarity index 94% rename from recipes/http-outgoingmessage-headers/tests/supported-methods/expected.ts rename to recipes/http-outgoingmessage-headers/tests/supported-methods/expected.cjs index 6a06b1c4..59d9871e 100644 --- a/recipes/http-outgoingmessage-headers/tests/supported-methods/expected.ts +++ b/recipes/http-outgoingmessage-headers/tests/supported-methods/expected.cjs @@ -1,4 +1,4 @@ -const http = require('node:http'); +const http = require('http'); const server = http.createServer((req, res) => { res.setHeader('content-type', 'application/json'); diff --git a/recipes/http-outgoingmessage-headers/tests/supported-methods/input.ts b/recipes/http-outgoingmessage-headers/tests/supported-methods/input.cjs similarity index 93% rename from recipes/http-outgoingmessage-headers/tests/supported-methods/input.ts rename to recipes/http-outgoingmessage-headers/tests/supported-methods/input.cjs index 5030e312..bb4a8804 100644 --- a/recipes/http-outgoingmessage-headers/tests/supported-methods/input.ts +++ b/recipes/http-outgoingmessage-headers/tests/supported-methods/input.cjs @@ -1,4 +1,4 @@ -const http = require('node:http'); +const http = require('http'); const server = http.createServer((req, res) => { res.setHeader('content-type', 'application/json'); diff --git a/utils/src/ast-grep/import-statement.test.ts b/utils/src/ast-grep/import-statement.test.ts index a3e77abb..0d7387a5 100644 --- a/utils/src/ast-grep/import-statement.test.ts +++ b/utils/src/ast-grep/import-statement.test.ts @@ -259,10 +259,8 @@ describe("import-statement", () => { `; const ast = astGrep.parse(astGrep.Lang.JavaScript, code); - const imports = getNodeImportStatements(ast, "test"); - assert.strictEqual(imports.length, 2); assert.deepStrictEqual( imports.map((i) => i.field("source")?.text()), ['"node:test"', '"test"'],