From 479adeed8662364281bbad60b50dad8ac4e5228b Mon Sep 17 00:00:00 2001 From: Sushant Date: Tue, 4 Aug 2026 23:04:44 -0700 Subject: [PATCH 1/7] fix(monday): fail closed when the webhook secret is missing verifyMondayWebhookSignature returned { valid: true } for an empty secret, matching the fail-open family already fixed in the Spotify, Zoom and Slack verifiers (#519, #520, #514). Refs #581 --- packages/monday/webhooks/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/monday/webhooks/types.ts b/packages/monday/webhooks/types.ts index 252722856..a6003e1da 100644 --- a/packages/monday/webhooks/types.ts +++ b/packages/monday/webhooks/types.ts @@ -127,7 +127,7 @@ export function verifyMondayWebhookSignature( secret: string, ): { valid: boolean; error?: string } { if (!secret) { - return { valid: true }; + return { valid: false, error: 'Missing webhook secret' }; } const rawBody = request.rawBody; From 0409bc157eed365898d3d6e17b00e441becd912f Mon Sep 17 00:00:00 2001 From: Sushant Date: Tue, 4 Aug 2026 23:04:49 -0700 Subject: [PATCH 2/7] fix(monday): verify webhook signatures before handling events The verification block was commented out in all three event handlers, so itemCreated, statusChanged and columnValueChanged accepted any request that matched the event shape -- including forged ones -- and itemCreated went on to write to the database. Enable the existing check in each handler, returning 401 before any DB write. The challenge handler is intentionally left unverified: it is the subscription handshake Monday sends before a secret is exchanged, and it performs no writes. Refs #581 --- .../monday/webhooks/column-value-changed.ts | 18 +++++++++--------- packages/monday/webhooks/item-created.ts | 18 +++++++++--------- packages/monday/webhooks/status-changed.ts | 18 +++++++++--------- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/packages/monday/webhooks/column-value-changed.ts b/packages/monday/webhooks/column-value-changed.ts index c91908907..3f7d1f34a 100644 --- a/packages/monday/webhooks/column-value-changed.ts +++ b/packages/monday/webhooks/column-value-changed.ts @@ -1,19 +1,19 @@ import { logEventFromContext } from 'corsair/core'; import type { MondayWebhooks } from '../index'; -import { createMondayMatch } from './types'; +import { createMondayMatch, verifyMondayWebhookSignature } from './types'; export const columnValueChanged: MondayWebhooks['columnValueChanged'] = { match: createMondayMatch('change_column_value'), handler: async (ctx, request) => { - // const verification = verifyMondayWebhookSignature(request, ctx.key); - // if (!verification.valid) { - // return { - // success: false, - // statusCode: 401, - // error: verification.error || 'Signature verification failed', - // }; - // } + const verification = verifyMondayWebhookSignature(request, ctx.key); + if (!verification.valid) { + return { + success: false, + statusCode: 401, + error: verification.error || 'Signature verification failed', + }; + } const event = request.payload.event; diff --git a/packages/monday/webhooks/item-created.ts b/packages/monday/webhooks/item-created.ts index afc0aae98..268b7ba45 100644 --- a/packages/monday/webhooks/item-created.ts +++ b/packages/monday/webhooks/item-created.ts @@ -1,19 +1,19 @@ import { logEventFromContext } from 'corsair/core'; import type { MondayWebhooks } from '../index'; -import { createMondayMatch } from './types'; +import { createMondayMatch, verifyMondayWebhookSignature } from './types'; export const itemCreated: MondayWebhooks['itemCreated'] = { match: createMondayMatch('create_pulse'), handler: async (ctx, request) => { - // const verification = verifyMondayWebhookSignature(request, ctx.key); - // if (!verification.valid) { - // return { - // success: false, - // statusCode: 401, - // error: verification.error || 'Signature verification failed', - // }; - // } + const verification = verifyMondayWebhookSignature(request, ctx.key); + if (!verification.valid) { + return { + success: false, + statusCode: 401, + error: verification.error || 'Signature verification failed', + }; + } const event = request.payload.event; diff --git a/packages/monday/webhooks/status-changed.ts b/packages/monday/webhooks/status-changed.ts index b72863f6f..8895ba6a4 100644 --- a/packages/monday/webhooks/status-changed.ts +++ b/packages/monday/webhooks/status-changed.ts @@ -1,19 +1,19 @@ import { logEventFromContext } from 'corsair/core'; import type { MondayWebhooks } from '../index'; -import { createMondayMatch } from './types'; +import { createMondayMatch, verifyMondayWebhookSignature } from './types'; export const statusChanged: MondayWebhooks['statusChanged'] = { match: createMondayMatch('change_status_column_value'), handler: async (ctx, request) => { - // const verification = verifyMondayWebhookSignature(request, ctx.key); - // if (!verification.valid) { - // return { - // success: false, - // statusCode: 401, - // error: verification.error || 'Signature verification failed', - // }; - // } + const verification = verifyMondayWebhookSignature(request, ctx.key); + if (!verification.valid) { + return { + success: false, + statusCode: 401, + error: verification.error || 'Signature verification failed', + }; + } const event = request.payload.event; From 0d5b026a0af0a74170c76da639535ca31765d4fc Mon Sep 17 00:00:00 2001 From: Sushant Date: Tue, 4 Aug 2026 23:05:02 -0700 Subject: [PATCH 3/7] test(monday): cover webhook signature rejection in all three handlers Adds packages/monday/webhooks/webhooks.test.ts: the verifier's fail-closed paths, and per-handler assertions that a forged signature returns 401 and that itemCreated performs no upsert on rejection. Also maps corsair/core in the package's jest config, as cloudinary already does, so the handler modules resolve under ts-jest -- without it the suite cannot import a handler at all. Refs #581 --- packages/monday/jest.config.cjs | 1 + packages/monday/webhooks/webhooks.test.ts | 168 ++++++++++++++++++++++ 2 files changed, 169 insertions(+) create mode 100644 packages/monday/webhooks/webhooks.test.ts diff --git a/packages/monday/jest.config.cjs b/packages/monday/jest.config.cjs index 296a927ca..8c6218f64 100644 --- a/packages/monday/jest.config.cjs +++ b/packages/monday/jest.config.cjs @@ -44,6 +44,7 @@ module.exports = { ], }, moduleNameMapper: { + '^corsair/core$': '/../corsair/core.ts', '^corsair/http$': '/../corsair/http.ts', '^(\\.\\.?/.*)\\.js$': '$1', }, diff --git a/packages/monday/webhooks/webhooks.test.ts b/packages/monday/webhooks/webhooks.test.ts new file mode 100644 index 000000000..b99ab2cf2 --- /dev/null +++ b/packages/monday/webhooks/webhooks.test.ts @@ -0,0 +1,168 @@ +import * as crypto from 'node:crypto'; +import type { WebhookRequest } from 'corsair/core'; +import type { MondayContext } from '../index'; +import { columnValueChanged } from './column-value-changed'; +import { itemCreated } from './item-created'; +import { statusChanged } from './status-changed'; +import { verifyMondayWebhookSignature } from './types'; + +const WEBHOOK_SECRET = 'monday-webhook-secret'; + +function sign(rawBody: string, secret: string): string { + return crypto.createHmac('sha256', secret).update(rawBody).digest('hex'); +} + +function makeWebhookRequest( + event: Record, + options?: { signature?: string; secret?: string }, +): WebhookRequest> { + const payload = { event }; + const rawBody = JSON.stringify(payload); + const signature = + options?.signature ?? sign(rawBody, options?.secret ?? WEBHOOK_SECRET); + + return { + payload, + rawBody, + headers: { authorization: signature }, + } as unknown as WebhookRequest>; +} + +function makeCtx(): MondayContext { + return { + key: WEBHOOK_SECRET, + db: { + items: { + upsertByEntityId: jest.fn().mockResolvedValue({ id: 'entity-1' }), + }, + }, + } as unknown as MondayContext; +} + +const createPulseEvent = { + type: 'create_pulse', + pulseId: 123, + pulseName: 'A task', + boardId: 456, + groupId: 'topics', + triggerTime: '2026-05-22T00:00:00Z', + userId: 789, +}; + +describe('verifyMondayWebhookSignature', () => { + it('should fail closed when the secret is missing', () => { + // The regression: an empty secret returned { valid: true }, so a + // deployment with no secret configured accepted forged events. + const result = verifyMondayWebhookSignature( + makeWebhookRequest(createPulseEvent), + '', + ); + expect(result).toEqual({ valid: false, error: 'Missing webhook secret' }); + }); + + it('should return invalid when the Authorization header is missing', () => { + const request = { + payload: { event: createPulseEvent }, + rawBody: JSON.stringify({ event: createPulseEvent }), + headers: {}, + } as unknown as WebhookRequest; + + const result = verifyMondayWebhookSignature(request, WEBHOOK_SECRET); + expect(result).toEqual({ + valid: false, + error: 'Missing Authorization header', + }); + }); + + it('should return invalid when the raw body is missing', () => { + const request = { + payload: { event: createPulseEvent }, + headers: { authorization: 'whatever' }, + } as unknown as WebhookRequest; + + const result = verifyMondayWebhookSignature(request, WEBHOOK_SECRET); + expect(result.valid).toBe(false); + expect(result.error).toMatch(/raw body/i); + }); + + it('should return valid for a correctly signed request', () => { + const result = verifyMondayWebhookSignature( + makeWebhookRequest(createPulseEvent), + WEBHOOK_SECRET, + ); + expect(result).toEqual({ valid: true }); + }); +}); + +describe('monday webhook handlers verify signatures', () => { + it('itemCreated rejects a forged signature with 401 and writes nothing', async () => { + const ctx = makeCtx(); + const result = await itemCreated.handler( + ctx, + makeWebhookRequest(createPulseEvent, { + secret: 'a-different-secret', + }) as never, + ); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.statusCode).toBe(401); + } + expect(ctx.db.items?.upsertByEntityId).not.toHaveBeenCalled(); + }); + + it('itemCreated rejects when no secret is configured, and writes nothing', async () => { + const ctx = { ...makeCtx(), key: '' } as MondayContext; + const result = await itemCreated.handler( + ctx, + makeWebhookRequest(createPulseEvent) as never, + ); + + expect(result.success).toBe(false); + expect(ctx.db.items?.upsertByEntityId).not.toHaveBeenCalled(); + }); + + it('itemCreated accepts a correctly signed request and persists the item', async () => { + const ctx = makeCtx(); + const result = await itemCreated.handler( + ctx, + makeWebhookRequest(createPulseEvent) as never, + ); + + expect(result.success).toBe(true); + expect(ctx.db.items?.upsertByEntityId).toHaveBeenCalledWith( + '123', + expect.objectContaining({ id: '123', name: 'A task' }), + ); + }); + + it('statusChanged rejects a forged signature with 401', async () => { + const result = await statusChanged.handler( + makeCtx(), + makeWebhookRequest( + { type: 'change_status_column_value', pulseId: 1, boardId: 2 }, + { secret: 'a-different-secret' }, + ) as never, + ); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.statusCode).toBe(401); + } + }); + + it('columnValueChanged rejects a forged signature with 401', async () => { + const result = await columnValueChanged.handler( + makeCtx(), + makeWebhookRequest( + { type: 'change_column_value', pulseId: 1, boardId: 2 }, + { secret: 'a-different-secret' }, + ) as never, + ); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.statusCode).toBe(401); + } + }); +}); From 060103878e375f3fee3423d9572a135fa3d3c748 Mon Sep 17 00:00:00 2001 From: Sushant Date: Tue, 4 Aug 2026 23:15:32 -0700 Subject: [PATCH 4/7] chore: re-run PR gate against the updated description From e6800dc1d9409f75a9f5c7d80e7acddca476150c Mon Sep 17 00:00:00 2001 From: ambikeesshh Date: Thu, 6 Aug 2026 08:13:44 +0530 Subject: [PATCH 5/7] fix(monday): verify board webhooks with JWT HS256 --- packages/monday/webhooks/types.ts | 98 +++++++++++++++++++++++++++---- 1 file changed, 85 insertions(+), 13 deletions(-) diff --git a/packages/monday/webhooks/types.ts b/packages/monday/webhooks/types.ts index a6003e1da..df9505dab 100644 --- a/packages/monday/webhooks/types.ts +++ b/packages/monday/webhooks/types.ts @@ -1,9 +1,9 @@ +import * as crypto from 'node:crypto'; import type { CorsairWebhookMatcher, RawWebhookRequest, WebhookRequest, } from 'corsair/core'; -import { verifyHmacSignature } from 'corsair/http'; import { z } from 'zod'; // ── Shared Sub-Schemas ──────────────────────────────────────────────────────── @@ -122,6 +122,85 @@ export function createMondayMatch(eventType: string): CorsairWebhookMatcher { // ── Signature Verification ──────────────────────────────────────────────────── +// Monday board / integration webhooks put an HS256 JWT in Authorization, +// signed with the app Signing Secret (or Client Secret for lifecycle events). +// See https://developer.monday.com/apps/docs/authorization-header +// This is NOT a body HMAC — treating the JWT as an HMAC digest 401s real traffic. + +function base64UrlEncode(buf: Buffer): string { + return buf + .toString('base64') + .replace(/=+$/g, '') + .replace(/\+/g, '-') + .replace(/\//g, '_'); +} + +function base64UrlDecode(input: string): Buffer { + const normalized = input.replace(/-/g, '+').replace(/_/g, '/'); + const padded = normalized.padEnd( + normalized.length + ((4 - (normalized.length % 4)) % 4), + '=', + ); + return Buffer.from(padded, 'base64'); +} + +function timingSafeEqualString(a: string, b: string): boolean { + const aBuf = Buffer.from(a); + const bBuf = Buffer.from(b); + if (aBuf.length !== bBuf.length) return false; + return crypto.timingSafeEqual(aBuf, bBuf); +} + +function verifyMondayJwt( + token: string, + secret: string, +): { valid: boolean; error?: string } { + const parts = token.split('.'); + if (parts.length !== 3 || !parts[0] || !parts[1] || !parts[2]) { + return { valid: false, error: 'Invalid Authorization JWT' }; + } + + const [headerB64, payloadB64, signatureB64] = parts; + + try { + const header = JSON.parse(base64UrlDecode(headerB64).toString('utf8')) as { + alg?: string; + }; + if (header.alg !== 'HS256') { + return { valid: false, error: 'Unsupported JWT algorithm' }; + } + } catch { + return { valid: false, error: 'Invalid Authorization JWT' }; + } + + const expectedSig = base64UrlEncode( + crypto + .createHmac('sha256', secret) + .update(`${headerB64}.${payloadB64}`) + .digest(), + ); + + if (!timingSafeEqualString(expectedSig, signatureB64)) { + return { valid: false, error: 'Invalid signature' }; + } + + try { + const payload = JSON.parse( + base64UrlDecode(payloadB64).toString('utf8'), + ) as { exp?: number }; + if ( + typeof payload.exp === 'number' && + Math.floor(Date.now() / 1000) >= payload.exp + ) { + return { valid: false, error: 'Token expired' }; + } + } catch { + return { valid: false, error: 'Invalid Authorization JWT' }; + } + + return { valid: true }; +} + export function verifyMondayWebhookSignature( request: WebhookRequest, secret: string, @@ -130,14 +209,6 @@ export function verifyMondayWebhookSignature( return { valid: false, error: 'Missing webhook secret' }; } - const rawBody = request.rawBody; - if (!rawBody) { - return { - valid: false, - error: 'Missing raw body for signature verification', - }; - } - const headers = request.headers; const authHeader = Array.isArray(headers['authorization']) ? headers['authorization'][0] @@ -147,8 +218,9 @@ export function verifyMondayWebhookSignature( return { valid: false, error: 'Missing Authorization header' }; } - const isValid = verifyHmacSignature(rawBody, secret, authHeader, 'sha256'); - return isValid - ? { valid: true } - : { valid: false, error: 'Invalid signature' }; + const token = authHeader.startsWith('Bearer ') + ? authHeader.slice('Bearer '.length) + : authHeader; + + return verifyMondayJwt(token, secret); } From 329c3b5944a3781ec2ce3e75b641dced9538689a Mon Sep 17 00:00:00 2001 From: ambikeesshh Date: Thu, 6 Aug 2026 08:13:44 +0530 Subject: [PATCH 6/7] test(monday): cover JWT webhook verification paths --- packages/monday/webhooks/webhooks.test.ts | 112 +++++++++++++++++----- 1 file changed, 87 insertions(+), 25 deletions(-) diff --git a/packages/monday/webhooks/webhooks.test.ts b/packages/monday/webhooks/webhooks.test.ts index b99ab2cf2..3efafc066 100644 --- a/packages/monday/webhooks/webhooks.test.ts +++ b/packages/monday/webhooks/webhooks.test.ts @@ -6,25 +6,68 @@ import { itemCreated } from './item-created'; import { statusChanged } from './status-changed'; import { verifyMondayWebhookSignature } from './types'; +jest.mock('corsair/core', () => { + const actual = jest.requireActual('corsair/core'); + return { + ...actual, + logEventFromContext: jest.fn().mockResolvedValue(null), + }; +}); + const WEBHOOK_SECRET = 'monday-webhook-secret'; -function sign(rawBody: string, secret: string): string { - return crypto.createHmac('sha256', secret).update(rawBody).digest('hex'); +function base64UrlEncode(buf: Buffer): string { + return buf + .toString('base64') + .replace(/=+$/g, '') + .replace(/\+/g, '-') + .replace(/\//g, '_'); +} + +function signMondayJwt( + payload: Record, + secret: string, +): string { + const header = base64UrlEncode( + Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })), + ); + const body = base64UrlEncode(Buffer.from(JSON.stringify(payload))); + const signature = base64UrlEncode( + crypto.createHmac('sha256', secret).update(`${header}.${body}`).digest(), + ); + return `${header}.${body}.${signature}`; } function makeWebhookRequest( event: Record, - options?: { signature?: string; secret?: string }, + options?: { + secret?: string; + authorization?: string; + omitAuthorization?: boolean; + payloadExtra?: Record; + expired?: boolean; + }, ): WebhookRequest> { - const payload = { event }; + const payload = { event, ...options?.payloadExtra }; const rawBody = JSON.stringify(payload); - const signature = - options?.signature ?? sign(rawBody, options?.secret ?? WEBHOOK_SECRET); + const now = Math.floor(Date.now() / 1000); + const jwtPayload = { + accountId: 1825529, + userId: 4012689, + aud: 'https://example.com/monday/webhook', + iat: now, + exp: options?.expired ? now - 60 : now + 5 * 60, + }; + + const authorization = options?.omitAuthorization + ? undefined + : (options?.authorization ?? + signMondayJwt(jwtPayload, options?.secret ?? WEBHOOK_SECRET)); return { payload, rawBody, - headers: { authorization: signature }, + headers: authorization ? { authorization } : {}, } as unknown as WebhookRequest>; } @@ -36,6 +79,8 @@ function makeCtx(): MondayContext { upsertByEntityId: jest.fn().mockResolvedValue({ id: 'entity-1' }), }, }, + database: {}, + $getAccountId: jest.fn().mockResolvedValue('account-1'), } as unknown as MondayContext; } @@ -51,8 +96,6 @@ const createPulseEvent = { describe('verifyMondayWebhookSignature', () => { it('should fail closed when the secret is missing', () => { - // The regression: an empty secret returned { valid: true }, so a - // deployment with no secret configured accepted forged events. const result = verifyMondayWebhookSignature( makeWebhookRequest(createPulseEvent), '', @@ -61,37 +104,56 @@ describe('verifyMondayWebhookSignature', () => { }); it('should return invalid when the Authorization header is missing', () => { - const request = { - payload: { event: createPulseEvent }, - rawBody: JSON.stringify({ event: createPulseEvent }), - headers: {}, - } as unknown as WebhookRequest; - - const result = verifyMondayWebhookSignature(request, WEBHOOK_SECRET); + const result = verifyMondayWebhookSignature( + makeWebhookRequest(createPulseEvent, { omitAuthorization: true }), + WEBHOOK_SECRET, + ); expect(result).toEqual({ valid: false, error: 'Missing Authorization header', }); }); - it('should return invalid when the raw body is missing', () => { - const request = { - payload: { event: createPulseEvent }, - headers: { authorization: 'whatever' }, - } as unknown as WebhookRequest; + it('should return invalid for a forged JWT', () => { + const result = verifyMondayWebhookSignature( + makeWebhookRequest(createPulseEvent, { secret: 'a-different-secret' }), + WEBHOOK_SECRET, + ); + expect(result).toEqual({ valid: false, error: 'Invalid signature' }); + }); - const result = verifyMondayWebhookSignature(request, WEBHOOK_SECRET); - expect(result.valid).toBe(false); - expect(result.error).toMatch(/raw body/i); + it('should return invalid for an expired JWT', () => { + const result = verifyMondayWebhookSignature( + makeWebhookRequest(createPulseEvent, { expired: true }), + WEBHOOK_SECRET, + ); + expect(result).toEqual({ valid: false, error: 'Token expired' }); }); - it('should return valid for a correctly signed request', () => { + it('should return valid for a correctly signed JWT', () => { const result = verifyMondayWebhookSignature( makeWebhookRequest(createPulseEvent), WEBHOOK_SECRET, ); expect(result).toEqual({ valid: true }); }); + + it('should accept Bearer-prefixed Authorization JWTs', () => { + const token = signMondayJwt( + { + accountId: 1, + exp: Math.floor(Date.now() / 1000) + 60, + }, + WEBHOOK_SECRET, + ); + const result = verifyMondayWebhookSignature( + makeWebhookRequest(createPulseEvent, { + authorization: `Bearer ${token}`, + }), + WEBHOOK_SECRET, + ); + expect(result).toEqual({ valid: true }); + }); }); describe('monday webhook handlers verify signatures', () => { From 6ddbf3136b3d0b64bf268420fb46cdf00f5d0df7 Mon Sep 17 00:00:00 2001 From: ambikeesshh Date: Thu, 6 Aug 2026 08:21:46 +0530 Subject: [PATCH 7/7] fix(monday): parse Bearer auth scheme case-insensitively --- packages/monday/webhooks/tenant-matcher.ts | 5 ++--- packages/monday/webhooks/types.ts | 5 ++--- packages/monday/webhooks/webhooks.test.ts | 17 +++++++++++++++++ 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/monday/webhooks/tenant-matcher.ts b/packages/monday/webhooks/tenant-matcher.ts index f13c219c8..7a6c106fe 100644 --- a/packages/monday/webhooks/tenant-matcher.ts +++ b/packages/monday/webhooks/tenant-matcher.ts @@ -7,9 +7,8 @@ function readJwtPayload( const authorization = getHeader(request.headers, 'authorization'); if (!authorization) return null; - const token = authorization.startsWith('Bearer ') - ? authorization.slice('Bearer '.length) - : authorization; + // RFC 9110: auth scheme token is case-insensitive ("Bearer" / "bearer" / …). + const token = authorization.replace(/^Bearer\s+/i, ''); const parts = token.split('.'); const payloadSegment = parts[1]; if (!payloadSegment) return null; diff --git a/packages/monday/webhooks/types.ts b/packages/monday/webhooks/types.ts index df9505dab..8d3214ac3 100644 --- a/packages/monday/webhooks/types.ts +++ b/packages/monday/webhooks/types.ts @@ -218,9 +218,8 @@ export function verifyMondayWebhookSignature( return { valid: false, error: 'Missing Authorization header' }; } - const token = authHeader.startsWith('Bearer ') - ? authHeader.slice('Bearer '.length) - : authHeader; + // RFC 9110: auth scheme token is case-insensitive ("Bearer" / "bearer" / …). + const token = authHeader.replace(/^Bearer\s+/i, ''); return verifyMondayJwt(token, secret); } diff --git a/packages/monday/webhooks/webhooks.test.ts b/packages/monday/webhooks/webhooks.test.ts index 3efafc066..ccc3edf0f 100644 --- a/packages/monday/webhooks/webhooks.test.ts +++ b/packages/monday/webhooks/webhooks.test.ts @@ -154,6 +154,23 @@ describe('verifyMondayWebhookSignature', () => { ); expect(result).toEqual({ valid: true }); }); + + it('should accept lowercase bearer-prefixed Authorization JWTs', () => { + const token = signMondayJwt( + { + accountId: 1, + exp: Math.floor(Date.now() / 1000) + 60, + }, + WEBHOOK_SECRET, + ); + const result = verifyMondayWebhookSignature( + makeWebhookRequest(createPulseEvent, { + authorization: `bearer ${token}`, + }), + WEBHOOK_SECRET, + ); + expect(result).toEqual({ valid: true }); + }); }); describe('monday webhook handlers verify signatures', () => {