From 8f0051c8168dd955133e2e0ee94bd6022ae6a15f Mon Sep 17 00:00:00 2001 From: Yizack Rangel Date: Sun, 2 Aug 2026 00:35:43 +0200 Subject: [PATCH 1/2] feat: add Cal.com webhook validator --- README.md | 3 ++- playground/.env.example | 3 +++ playground/nuxt.config.ts | 3 +++ playground/server/api/webhooks/calcom.post.ts | 7 ++++++ src/module.ts | 4 +++ .../server/lib/validators/calcom.post.ts | 23 +++++++++++++++++ test/events.ts | 1 + test/fixtures/basic/nuxt.config.ts | 3 +++ test/simulations/calcom.ts | 25 +++++++++++++++++++ 9 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 playground/server/api/webhooks/calcom.post.ts create mode 100644 src/runtime/server/lib/validators/calcom.post.ts create mode 100644 test/simulations/calcom.ts diff --git a/README.md b/README.md index 67bcc6c..bf17a2c 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ A simple nuxt module that works on the edge to easily validate incoming webhooks ## Features -- 23 [Webhook validators](#supported-webhook-validators) +- 24 [Webhook validators](#supported-webhook-validators) - Works on the edge - Exposed [Server utils](#server-utils) @@ -80,6 +80,7 @@ Go to [playground/.env.example](./playground/.env.example) or [playground/nuxt.c - Bitbucket - Brevo +- Cal.com - Discord - Dropbox - Fourthwall diff --git a/playground/.env.example b/playground/.env.example index e66ce95..c79bc9c 100644 --- a/playground/.env.example +++ b/playground/.env.example @@ -4,6 +4,9 @@ NUXT_WEBHOOK_BITBUCKET_SECRET_KEY= # Brevo Validator NUXT_WEBHOOK_BREVO_TOKEN= +# Cal.com Validator +NUXT_WEBHOOK_CALCOM_SECRET_KEY= + # Discord Validator NUXT_WEBHOOK_DISCORD_PUBLIC_KEY= diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts index d4abe50..5f258a0 100644 --- a/playground/nuxt.config.ts +++ b/playground/nuxt.config.ts @@ -14,6 +14,9 @@ export default defineNuxtConfig({ brevo: { token: '', }, + calcom: { + secretKey: '', + }, discord: { publicKey: '', }, diff --git a/playground/server/api/webhooks/calcom.post.ts b/playground/server/api/webhooks/calcom.post.ts new file mode 100644 index 0000000..5c67e7a --- /dev/null +++ b/playground/server/api/webhooks/calcom.post.ts @@ -0,0 +1,7 @@ +export default defineEventHandler(async (event) => { + const isValidWebhook = await isValidCalcomWebhook(event) + + if (!isValidWebhook) throw createError({ status: 401, message: 'Unauthorized: webhook is not valid' }) + + return { isValidWebhook } +}) diff --git a/src/module.ts b/src/module.ts index 038c485..1665106 100644 --- a/src/module.ts +++ b/src/module.ts @@ -28,6 +28,10 @@ export default defineNuxtModule({ runtimeConfig.webhook.brevo = defu(runtimeConfig.webhook.brevo, { token: '', }) + // Cal.com Webhook + runtimeConfig.webhook.calcom = defu(runtimeConfig.webhook.calcom, { + secretKey: '', + }) // Discord Webhook runtimeConfig.webhook.discord = defu(runtimeConfig.webhook.discord, { publicKey: '', diff --git a/src/runtime/server/lib/validators/calcom.post.ts b/src/runtime/server/lib/validators/calcom.post.ts new file mode 100644 index 0000000..1a5a96f --- /dev/null +++ b/src/runtime/server/lib/validators/calcom.post.ts @@ -0,0 +1,23 @@ +import { type H3Event, getRequestHeaders } from 'h3' +import { computeSignature, HMAC_SHA256, ensureConfiguration, readRawBodyClone } from '../utils' + +const CALCOM_SIGNATURE = 'X-Cal-Signature-256'.toLowerCase() + +/** + * Validates Cal.com webhooks on the Edge + * @see {@link https://cal.com/docs/developing/guides/automation/webhooks#verifying-the-authenticity-of-the-received-payload} + * @param event H3Event + * @returns {boolean} `true` if the webhook is valid, `false` otherwise + */ +export const isValidCalcomWebhook = async (event: H3Event): Promise => { + const config = ensureConfiguration('calcom', event) + + const headers = getRequestHeaders(event) + const body = await readRawBodyClone(event) + const signature = headers[CALCOM_SIGNATURE] + + if (!signature || !body) return false + + const computedHash = await computeSignature(config.secretKey, HMAC_SHA256, body) + return signature === computedHash +} diff --git a/test/events.ts b/test/events.ts index 04a65fd..0179cae 100644 --- a/test/events.ts +++ b/test/events.ts @@ -1,5 +1,6 @@ export { simulateBitbucketEvent } from './simulations/bitbucket' export { simulateBrevoEvent } from './simulations/brevo' +export { simulateCalcomEvent } from './simulations/calcom' export { simulateDiscordEvent } from './simulations/discord' export { simulateDropboxEvent } from './simulations/dropbox' export { simulateFourthwallEvent } from './simulations/fourthwall' diff --git a/test/fixtures/basic/nuxt.config.ts b/test/fixtures/basic/nuxt.config.ts index c060e9b..f8c8093 100644 --- a/test/fixtures/basic/nuxt.config.ts +++ b/test/fixtures/basic/nuxt.config.ts @@ -13,6 +13,9 @@ export default defineNuxtConfig({ brevo: { token: 'testToken', }, + calcom: { + secretKey: 'testCalcomSecretKey', + }, discord: { publicKey: 'fcf4594ff55a5898a7e7ce541b93dc8ce618c7a4fa96ab7efd1ac2890571345c', }, diff --git a/test/simulations/calcom.ts b/test/simulations/calcom.ts new file mode 100644 index 0000000..939830f --- /dev/null +++ b/test/simulations/calcom.ts @@ -0,0 +1,25 @@ +import { subtle } from 'node:crypto' +import { Buffer } from 'node:buffer' +import { $fetch } from '@nuxt/test-utils/e2e' +import { encoder, HMAC_SHA256 } from '../../src/runtime/server/lib/utils' +import nuxtConfig from '../fixtures/basic/nuxt.config' + +const body = { data: 'testBody' } +const secretKey = nuxtConfig.runtimeConfig?.webhook?.calcom?.secretKey + +export const simulateCalcomEvent = async () => { + const signature = await subtle.importKey('raw', encoder.encode(secretKey), HMAC_SHA256, false, ['sign']) + const hmac = await subtle.sign(HMAC_SHA256.name, signature, encoder.encode(JSON.stringify(body))) + const computedHash = Buffer.from(hmac).toString('hex') + const validSignature = `sha256=${computedHash}` + + const headers = { + 'X-Cal-Signature-256': validSignature, + } + + return $fetch<{ isValidWebhook: boolean }>('/api/webhooks/calcom', { + method: 'POST', + headers, + body, + }) +} From 9fafc9877b753569f3019ebfc3a0251cf279b02d Mon Sep 17 00:00:00 2001 From: Yizack Rangel Date: Sun, 2 Aug 2026 00:40:48 +0200 Subject: [PATCH 2/2] test: fix calcom valid signature sent --- test/simulations/calcom.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/simulations/calcom.ts b/test/simulations/calcom.ts index 939830f..e8b3119 100644 --- a/test/simulations/calcom.ts +++ b/test/simulations/calcom.ts @@ -10,8 +10,7 @@ const secretKey = nuxtConfig.runtimeConfig?.webhook?.calcom?.secretKey export const simulateCalcomEvent = async () => { const signature = await subtle.importKey('raw', encoder.encode(secretKey), HMAC_SHA256, false, ['sign']) const hmac = await subtle.sign(HMAC_SHA256.name, signature, encoder.encode(JSON.stringify(body))) - const computedHash = Buffer.from(hmac).toString('hex') - const validSignature = `sha256=${computedHash}` + const validSignature = Buffer.from(hmac).toString('hex') const headers = { 'X-Cal-Signature-256': validSignature,