refactor(webhooks): move signature validators into provider handler files

Co-locate each validate*Signature function with its provider handler,
eliminating the circular dependency where handlers imported back from
utils.server.ts. validateJiraSignature is exported from jira.ts for
shared use by confluence.ts.
This commit is contained in:
Waleed Latif
2026-04-05 10:03:07 -07:00
parent 5d9b95a904
commit 7b6b50bbd2
13 changed files with 235 additions and 465 deletions

View File

@@ -1,6 +1,23 @@
import crypto from 'crypto'
import { createLogger } from '@sim/logger'
import { safeCompare } from '@/lib/core/security/encryption'
import type { WebhookProviderHandler } from '@/lib/webhooks/providers/types'
import { createHmacVerifier } from '@/lib/webhooks/providers/utils'
import { validateAshbySignature } from '@/lib/webhooks/utils.server'
const logger = createLogger('WebhookProvider:Ashby')
function validateAshbySignature(secretToken: string, signature: string, body: string): boolean {
try {
if (!secretToken || !signature || !body) { return false }
if (!signature.startsWith('sha256=')) { return false }
const providedSignature = signature.substring(7)
const computedHash = crypto.createHmac('sha256', secretToken).update(body, 'utf8').digest('hex')
return safeCompare(computedHash, providedSignature)
} catch (error) {
logger.error('Error validating Ashby signature:', error)
return false
}
}
export const ashbyHandler: WebhookProviderHandler = {
verifyAuth: createHmacVerifier({

View File

@@ -1,14 +1,30 @@
import crypto from 'crypto'
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { safeCompare } from '@/lib/core/security/encryption'
import type {
AuthContext,
EventMatchContext,
WebhookProviderHandler,
} from '@/lib/webhooks/providers/types'
import { validateAttioSignature } from '@/lib/webhooks/utils.server'
const logger = createLogger('WebhookProvider:Attio')
function validateAttioSignature(secret: string, signature: string, body: string): boolean {
try {
if (!secret || !signature || !body) {
logger.warn('Attio signature validation missing required fields', { hasSecret: !!secret, hasSignature: !!signature, hasBody: !!body })
return false
}
const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex')
logger.debug('Attio signature comparison', { computedSignature: `${computedHash.substring(0, 10)}...`, providedSignature: `${signature.substring(0, 10)}...`, computedLength: computedHash.length, providedLength: signature.length, match: computedHash === signature })
return safeCompare(computedHash, signature)
} catch (error) {
logger.error('Error validating Attio signature:', error)
return false
}
}
export const attioHandler: WebhookProviderHandler = {
verifyAuth({ webhook, request, rawBody, requestId, providerConfig }: AuthContext) {
const secret = providerConfig.webhookSecret as string | undefined

View File

@@ -1,6 +1,28 @@
import crypto from 'crypto'
import { createLogger } from '@sim/logger'
import { safeCompare } from '@/lib/core/security/encryption'
import type { WebhookProviderHandler } from '@/lib/webhooks/providers/types'
import { createHmacVerifier } from '@/lib/webhooks/providers/utils'
import { validateCalcomSignature } from '@/lib/webhooks/utils.server'
const logger = createLogger('WebhookProvider:Calcom')
function validateCalcomSignature(secret: string, signature: string, body: string): boolean {
try {
if (!secret || !signature || !body) {
logger.warn('Cal.com signature validation missing required fields', { hasSecret: !!secret, hasSignature: !!signature, hasBody: !!body })
return false
}
let providedSignature: string
if (signature.startsWith('sha256=')) { providedSignature = signature.substring(7) }
else { providedSignature = signature }
const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex')
logger.debug('Cal.com signature comparison', { computedSignature: `${computedHash.substring(0, 10)}...`, providedSignature: `${providedSignature.substring(0, 10)}...`, computedLength: computedHash.length, providedLength: providedSignature.length, match: computedHash === providedSignature })
return safeCompare(computedHash, providedSignature)
} catch (error) {
logger.error('Error validating Cal.com signature:', error)
return false
}
}
export const calcomHandler: WebhookProviderHandler = {
verifyAuth: createHmacVerifier({

View File

@@ -1,6 +1,25 @@
import crypto from 'crypto'
import { createLogger } from '@sim/logger'
import { safeCompare } from '@/lib/core/security/encryption'
import type { WebhookProviderHandler } from '@/lib/webhooks/providers/types'
import { createHmacVerifier } from '@/lib/webhooks/providers/utils'
import { validateCirclebackSignature } from '@/lib/webhooks/utils.server'
const logger = createLogger('WebhookProvider:Circleback')
function validateCirclebackSignature(secret: string, signature: string, body: string): boolean {
try {
if (!secret || !signature || !body) {
logger.warn('Circleback signature validation missing required fields', { hasSecret: !!secret, hasSignature: !!signature, hasBody: !!body })
return false
}
const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex')
logger.debug('Circleback signature comparison', { computedSignature: `${computedHash.substring(0, 10)}...`, providedSignature: `${signature.substring(0, 10)}...`, computedLength: computedHash.length, providedLength: signature.length, match: computedHash === signature })
return safeCompare(computedHash, signature)
} catch (error) {
logger.error('Error validating Circleback signature:', error)
return false
}
}
export const circlebackHandler: WebhookProviderHandler = {
verifyAuth: createHmacVerifier({

View File

@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import type { EventMatchContext, WebhookProviderHandler } from '@/lib/webhooks/providers/types'
import { createHmacVerifier } from '@/lib/webhooks/providers/utils'
import { validateJiraSignature } from '@/lib/webhooks/utils.server'
import { validateJiraSignature } from '@/lib/webhooks/providers/jira'
const logger = createLogger('WebhookProvider:Confluence')

View File

@@ -1,6 +1,30 @@
import crypto from 'crypto'
import { createLogger } from '@sim/logger'
import { safeCompare } from '@/lib/core/security/encryption'
import type { WebhookProviderHandler } from '@/lib/webhooks/providers/types'
import { createHmacVerifier } from '@/lib/webhooks/providers/utils'
import { validateFirefliesSignature } from '@/lib/webhooks/utils.server'
const logger = createLogger('WebhookProvider:Fireflies')
function validateFirefliesSignature(secret: string, signature: string, body: string): boolean {
try {
if (!secret || !signature || !body) {
logger.warn('Fireflies signature validation missing required fields', { hasSecret: !!secret, hasSignature: !!signature, hasBody: !!body })
return false
}
if (!signature.startsWith('sha256=')) {
logger.warn('Fireflies signature has invalid format (expected sha256=)', { signaturePrefix: signature.substring(0, 10) })
return false
}
const providedSignature = signature.substring(7)
const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex')
logger.debug('Fireflies signature comparison', { computedSignature: `${computedHash.substring(0, 10)}...`, providedSignature: `${providedSignature.substring(0, 10)}...`, computedLength: computedHash.length, providedLength: providedSignature.length, match: computedHash === providedSignature })
return safeCompare(computedHash, providedSignature)
} catch (error) {
logger.error('Error validating Fireflies signature:', error)
return false
}
}
export const firefliesHandler: WebhookProviderHandler = {
verifyAuth: createHmacVerifier({

View File

@@ -1,14 +1,42 @@
import crypto from 'crypto'
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { safeCompare } from '@/lib/core/security/encryption'
import type {
AuthContext,
EventMatchContext,
WebhookProviderHandler,
} from '@/lib/webhooks/providers/types'
import { validateGitHubSignature } from '@/lib/webhooks/utils.server'
const logger = createLogger('WebhookProvider:GitHub')
function validateGitHubSignature(secret: string, signature: string, body: string): boolean {
try {
if (!secret || !signature || !body) {
logger.warn('GitHub signature validation missing required fields', { hasSecret: !!secret, hasSignature: !!signature, hasBody: !!body })
return false
}
let algorithm: 'sha256' | 'sha1'
let providedSignature: string
if (signature.startsWith('sha256=')) {
algorithm = 'sha256'
providedSignature = signature.substring(7)
} else if (signature.startsWith('sha1=')) {
algorithm = 'sha1'
providedSignature = signature.substring(5)
} else {
logger.warn('GitHub signature has invalid format', { signature: `${signature.substring(0, 10)}...` })
return false
}
const computedHash = crypto.createHmac(algorithm, secret).update(body, 'utf8').digest('hex')
logger.debug('GitHub signature comparison', { algorithm, computedSignature: `${computedHash.substring(0, 10)}...`, providedSignature: `${providedSignature.substring(0, 10)}...`, computedLength: computedHash.length, providedLength: providedSignature.length, match: computedHash === providedSignature })
return safeCompare(computedHash, providedSignature)
} catch (error) {
logger.error('Error validating GitHub signature:', error)
return false
}
}
export const githubHandler: WebhookProviderHandler = {
verifyAuth({ request, rawBody, requestId, providerConfig }: AuthContext) {
const secret = providerConfig.webhookSecret as string | undefined

View File

@@ -1,10 +1,31 @@
import crypto from 'crypto'
import { createLogger } from '@sim/logger'
import { safeCompare } from '@/lib/core/security/encryption'
import type { EventMatchContext, WebhookProviderHandler } from '@/lib/webhooks/providers/types'
import { createHmacVerifier } from '@/lib/webhooks/providers/utils'
import { validateJiraSignature } from '@/lib/webhooks/utils.server'
const logger = createLogger('WebhookProvider:Jira')
export function validateJiraSignature(secret: string, signature: string, body: string): boolean {
try {
if (!secret || !signature || !body) {
logger.warn('Jira signature validation missing required fields', { hasSecret: !!secret, hasSignature: !!signature, hasBody: !!body })
return false
}
if (!signature.startsWith('sha256=')) {
logger.warn('Jira signature has invalid format (expected sha256=)', { signaturePrefix: signature.substring(0, 10) })
return false
}
const providedSignature = signature.substring(7)
const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex')
logger.debug('Jira signature comparison', { computedSignature: `${computedHash.substring(0, 10)}...`, providedSignature: `${providedSignature.substring(0, 10)}...`, computedLength: computedHash.length, providedLength: providedSignature.length, match: computedHash === providedSignature })
return safeCompare(computedHash, providedSignature)
} catch (error) {
logger.error('Error validating Jira signature:', error)
return false
}
}
export const jiraHandler: WebhookProviderHandler = {
verifyAuth: createHmacVerifier({
configKey: 'webhookSecret',

View File

@@ -1,6 +1,25 @@
import crypto from 'crypto'
import { createLogger } from '@sim/logger'
import { safeCompare } from '@/lib/core/security/encryption'
import type { WebhookProviderHandler } from '@/lib/webhooks/providers/types'
import { createHmacVerifier } from '@/lib/webhooks/providers/utils'
import { validateLinearSignature } from '@/lib/webhooks/utils.server'
const logger = createLogger('WebhookProvider:Linear')
function validateLinearSignature(secret: string, signature: string, body: string): boolean {
try {
if (!secret || !signature || !body) {
logger.warn('Linear signature validation missing required fields', { hasSecret: !!secret, hasSignature: !!signature, hasBody: !!body })
return false
}
const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex')
logger.debug('Linear signature comparison', { computedSignature: `${computedHash.substring(0, 10)}...`, providedSignature: `${signature.substring(0, 10)}...`, computedLength: computedHash.length, providedLength: signature.length, match: computedHash === signature })
return safeCompare(computedHash, signature)
} catch (error) {
logger.error('Error validating Linear signature:', error)
return false
}
}
export const linearHandler: WebhookProviderHandler = {
verifyAuth: createHmacVerifier({

View File

@@ -1,14 +1,30 @@
import crypto from 'crypto'
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { safeCompare } from '@/lib/core/security/encryption'
import type {
AuthContext,
EventFilterContext,
WebhookProviderHandler,
} from '@/lib/webhooks/providers/types'
import { validateMicrosoftTeamsSignature } from '@/lib/webhooks/utils.server'
const logger = createLogger('WebhookProvider:MicrosoftTeams')
function validateMicrosoftTeamsSignature(hmacSecret: string, signature: string, body: string): boolean {
try {
if (!hmacSecret || !signature || !body) { return false }
if (!signature.startsWith('HMAC ')) { return false }
const providedSignature = signature.substring(5)
const secretBytes = Buffer.from(hmacSecret, 'base64')
const bodyBytes = Buffer.from(body, 'utf8')
const computedHash = crypto.createHmac('sha256', secretBytes).update(bodyBytes).digest('base64')
return safeCompare(computedHash, providedSignature)
} catch (error) {
logger.error('Error validating Microsoft Teams signature:', error)
return false
}
}
function parseFirstNotification(
body: unknown
): { subscriptionId: string; messageId: string } | null {

View File

@@ -1,11 +1,35 @@
import crypto from 'crypto'
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { safeCompare } from '@/lib/core/security/encryption'
import type { AuthContext, WebhookProviderHandler } from '@/lib/webhooks/providers/types'
import { convertSquareBracketsToTwiML } from '@/lib/webhooks/utils'
import { validateTwilioSignature } from '@/lib/webhooks/utils.server'
const logger = createLogger('WebhookProvider:TwilioVoice')
async function validateTwilioSignature(authToken: string, signature: string, url: string, params: Record<string, unknown>): Promise<boolean> {
try {
if (!authToken || !signature || !url) {
logger.warn('Twilio signature validation missing required fields', { hasAuthToken: !!authToken, hasSignature: !!signature, hasUrl: !!url })
return false
}
const sortedKeys = Object.keys(params).sort()
let data = url
for (const key of sortedKeys) { data += key + params[key] }
logger.debug('Twilio signature validation string built', { url, sortedKeys, dataLength: data.length })
const encoder = new TextEncoder()
const key = await crypto.subtle.importKey('raw', encoder.encode(authToken), { name: 'HMAC', hash: 'SHA-1' }, false, ['sign'])
const signatureBytes = await crypto.subtle.sign('HMAC', key, encoder.encode(data))
const signatureArray = Array.from(new Uint8Array(signatureBytes))
const signatureBase64 = btoa(String.fromCharCode(...signatureArray))
logger.debug('Twilio signature comparison', { computedSignature: `${signatureBase64.substring(0, 10)}...`, providedSignature: `${signature.substring(0, 10)}...`, computedLength: signatureBase64.length, providedLength: signature.length, match: signatureBase64 === signature })
return safeCompare(signatureBase64, signature)
} catch (error) {
logger.error('Error validating Twilio signature:', error)
return false
}
}
function getExternalUrl(request: Request): string {
const proto = request.headers.get('x-forwarded-proto') || 'https'
const host = request.headers.get('x-forwarded-host') || request.headers.get('host')

View File

@@ -1,6 +1,23 @@
import crypto from 'crypto'
import { createLogger } from '@sim/logger'
import { safeCompare } from '@/lib/core/security/encryption'
import type { WebhookProviderHandler } from '@/lib/webhooks/providers/types'
import { createHmacVerifier } from '@/lib/webhooks/providers/utils'
import { validateTypeformSignature } from '@/lib/webhooks/utils.server'
const logger = createLogger('WebhookProvider:Typeform')
function validateTypeformSignature(secret: string, signature: string, body: string): boolean {
try {
if (!secret || !signature || !body) { return false }
if (!signature.startsWith('sha256=')) { return false }
const providedSignature = signature.substring(7)
const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('base64')
return safeCompare(computedHash, providedSignature)
} catch (error) {
logger.error('Error validating Typeform signature:', error)
return false
}
}
export const typeformHandler: WebhookProviderHandler = {
verifyAuth: createHmacVerifier({

View File

@@ -481,63 +481,6 @@ async function formatTeamsGraphNotification(
}
}
export async function validateTwilioSignature(
authToken: string,
signature: string,
url: string,
params: Record<string, any>
): Promise<boolean> {
try {
if (!authToken || !signature || !url) {
logger.warn('Twilio signature validation missing required fields', {
hasAuthToken: !!authToken,
hasSignature: !!signature,
hasUrl: !!url,
})
return false
}
const sortedKeys = Object.keys(params).sort()
let data = url
for (const key of sortedKeys) {
data += key + params[key]
}
logger.debug('Twilio signature validation string built', {
url,
sortedKeys,
dataLength: data.length,
})
const encoder = new TextEncoder()
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(authToken),
{ name: 'HMAC', hash: 'SHA-1' },
false,
['sign']
)
const signatureBytes = await crypto.subtle.sign('HMAC', key, encoder.encode(data))
const signatureArray = Array.from(new Uint8Array(signatureBytes))
const signatureBase64 = btoa(String.fromCharCode(...signatureArray))
logger.debug('Twilio signature comparison', {
computedSignature: `${signatureBase64.substring(0, 10)}...`,
providedSignature: `${signature.substring(0, 10)}...`,
computedLength: signatureBase64.length,
providedLength: signature.length,
match: signatureBase64 === signature,
})
return safeCompare(signatureBase64, signature)
} catch (error) {
logger.error('Error validating Twilio signature:', error)
return false
}
}
const SLACK_MAX_FILE_SIZE = 50 * 1024 * 1024 // 50 MB
const SLACK_MAX_FILES = 15
@@ -1359,357 +1302,6 @@ export async function formatWebhookInput(
return body
}
/**
* Validates a Microsoft Teams outgoing webhook request signature using HMAC SHA-256
* @param hmacSecret - Microsoft Teams HMAC secret (base64 encoded)
* @param signature - Authorization header value (should start with 'HMAC ')
* @param body - Raw request body string
* @returns Whether the signature is valid
*/
export function validateMicrosoftTeamsSignature(
hmacSecret: string,
signature: string,
body: string
): boolean {
try {
if (!hmacSecret || !signature || !body) {
return false
}
if (!signature.startsWith('HMAC ')) {
return false
}
const providedSignature = signature.substring(5)
const secretBytes = Buffer.from(hmacSecret, 'base64')
const bodyBytes = Buffer.from(body, 'utf8')
const computedHash = crypto.createHmac('sha256', secretBytes).update(bodyBytes).digest('base64')
return safeCompare(computedHash, providedSignature)
} catch (error) {
logger.error('Error validating Microsoft Teams signature:', error)
return false
}
}
/**
* Validates a Typeform webhook request signature using HMAC SHA-256
* @param secret - Typeform webhook secret (plain text)
* @param signature - Typeform-Signature header value (should be in format 'sha256=<signature>')
* @param body - Raw request body string
* @returns Whether the signature is valid
*/
export function validateTypeformSignature(
secret: string,
signature: string,
body: string
): boolean {
try {
if (!secret || !signature || !body) {
return false
}
if (!signature.startsWith('sha256=')) {
return false
}
const providedSignature = signature.substring(7)
const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('base64')
return safeCompare(computedHash, providedSignature)
} catch (error) {
logger.error('Error validating Typeform signature:', error)
return false
}
}
/**
* Validates a Linear webhook request signature using HMAC SHA-256
* @param secret - Linear webhook secret (plain text)
* @param signature - Linear-Signature header value (hex-encoded HMAC SHA-256 signature)
* @param body - Raw request body string
* @returns Whether the signature is valid
*/
export function validateLinearSignature(secret: string, signature: string, body: string): boolean {
try {
if (!secret || !signature || !body) {
logger.warn('Linear signature validation missing required fields', {
hasSecret: !!secret,
hasSignature: !!signature,
hasBody: !!body,
})
return false
}
const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex')
logger.debug('Linear signature comparison', {
computedSignature: `${computedHash.substring(0, 10)}...`,
providedSignature: `${signature.substring(0, 10)}...`,
computedLength: computedHash.length,
providedLength: signature.length,
match: computedHash === signature,
})
return safeCompare(computedHash, signature)
} catch (error) {
logger.error('Error validating Linear signature:', error)
return false
}
}
/**
* Validates an Attio webhook request signature using HMAC SHA-256
* @param secret - Attio webhook signing secret (plain text)
* @param signature - Attio-Signature header value (hex-encoded HMAC SHA-256 signature)
* @param body - Raw request body string
* @returns Whether the signature is valid
*/
export function validateAttioSignature(secret: string, signature: string, body: string): boolean {
try {
if (!secret || !signature || !body) {
logger.warn('Attio signature validation missing required fields', {
hasSecret: !!secret,
hasSignature: !!signature,
hasBody: !!body,
})
return false
}
const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex')
logger.debug('Attio signature comparison', {
computedSignature: `${computedHash.substring(0, 10)}...`,
providedSignature: `${signature.substring(0, 10)}...`,
computedLength: computedHash.length,
providedLength: signature.length,
match: computedHash === signature,
})
return safeCompare(computedHash, signature)
} catch (error) {
logger.error('Error validating Attio signature:', error)
return false
}
}
/**
* Validates a Circleback webhook request signature using HMAC SHA-256
* @param secret - Circleback signing secret (plain text)
* @param signature - x-signature header value (hex-encoded HMAC SHA-256 signature)
* @param body - Raw request body string
* @returns Whether the signature is valid
*/
export function validateCirclebackSignature(
secret: string,
signature: string,
body: string
): boolean {
try {
if (!secret || !signature || !body) {
logger.warn('Circleback signature validation missing required fields', {
hasSecret: !!secret,
hasSignature: !!signature,
hasBody: !!body,
})
return false
}
const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex')
logger.debug('Circleback signature comparison', {
computedSignature: `${computedHash.substring(0, 10)}...`,
providedSignature: `${signature.substring(0, 10)}...`,
computedLength: computedHash.length,
providedLength: signature.length,
match: computedHash === signature,
})
return safeCompare(computedHash, signature)
} catch (error) {
logger.error('Error validating Circleback signature:', error)
return false
}
}
/**
* Validates a Jira webhook request signature using HMAC SHA-256
* @param secret - Jira webhook secret (plain text)
* @param signature - X-Hub-Signature header value (format: 'sha256=<hex>')
* @param body - Raw request body string
* @returns Whether the signature is valid
*/
export function validateJiraSignature(secret: string, signature: string, body: string): boolean {
try {
if (!secret || !signature || !body) {
logger.warn('Jira signature validation missing required fields', {
hasSecret: !!secret,
hasSignature: !!signature,
hasBody: !!body,
})
return false
}
if (!signature.startsWith('sha256=')) {
logger.warn('Jira signature has invalid format (expected sha256=)', {
signaturePrefix: signature.substring(0, 10),
})
return false
}
const providedSignature = signature.substring(7)
const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex')
logger.debug('Jira signature comparison', {
computedSignature: `${computedHash.substring(0, 10)}...`,
providedSignature: `${providedSignature.substring(0, 10)}...`,
computedLength: computedHash.length,
providedLength: providedSignature.length,
match: computedHash === providedSignature,
})
return safeCompare(computedHash, providedSignature)
} catch (error) {
logger.error('Error validating Jira signature:', error)
return false
}
}
/**
* Validates a Fireflies webhook request signature using HMAC SHA-256
* @param secret - Fireflies webhook secret (16-32 characters)
* @param signature - x-hub-signature header value (format: 'sha256=<hex>')
* @param body - Raw request body string
* @returns Whether the signature is valid
*/
export function validateFirefliesSignature(
secret: string,
signature: string,
body: string
): boolean {
try {
if (!secret || !signature || !body) {
logger.warn('Fireflies signature validation missing required fields', {
hasSecret: !!secret,
hasSignature: !!signature,
hasBody: !!body,
})
return false
}
if (!signature.startsWith('sha256=')) {
logger.warn('Fireflies signature has invalid format (expected sha256=)', {
signaturePrefix: signature.substring(0, 10),
})
return false
}
const providedSignature = signature.substring(7)
const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex')
logger.debug('Fireflies signature comparison', {
computedSignature: `${computedHash.substring(0, 10)}...`,
providedSignature: `${providedSignature.substring(0, 10)}...`,
computedLength: computedHash.length,
providedLength: providedSignature.length,
match: computedHash === providedSignature,
})
return safeCompare(computedHash, providedSignature)
} catch (error) {
logger.error('Error validating Fireflies signature:', error)
return false
}
}
/**
* Validates an Ashby webhook signature using HMAC-SHA256.
* Ashby signs payloads with the secretToken and sends the digest in the Ashby-Signature header.
* @param secretToken - The secret token configured when creating the webhook
* @param signature - Ashby-Signature header value (format: 'sha256=<hex>')
* @param body - Raw request body string
* @returns Whether the signature is valid
*/
export function validateAshbySignature(
secretToken: string,
signature: string,
body: string
): boolean {
try {
if (!secretToken || !signature || !body) {
return false
}
if (!signature.startsWith('sha256=')) {
return false
}
const providedSignature = signature.substring(7)
const computedHash = crypto.createHmac('sha256', secretToken).update(body, 'utf8').digest('hex')
return safeCompare(computedHash, providedSignature)
} catch (error) {
logger.error('Error validating Ashby signature:', error)
return false
}
}
/**
* Validates a GitHub webhook request signature using HMAC SHA-256 or SHA-1
* @param secret - GitHub webhook secret (plain text)
* @param signature - X-Hub-Signature-256 or X-Hub-Signature header value (format: 'sha256=<hex>' or 'sha1=<hex>')
* @param body - Raw request body string
* @returns Whether the signature is valid
*/
export function validateGitHubSignature(secret: string, signature: string, body: string): boolean {
try {
if (!secret || !signature || !body) {
logger.warn('GitHub signature validation missing required fields', {
hasSecret: !!secret,
hasSignature: !!signature,
hasBody: !!body,
})
return false
}
let algorithm: 'sha256' | 'sha1'
let providedSignature: string
if (signature.startsWith('sha256=')) {
algorithm = 'sha256'
providedSignature = signature.substring(7)
} else if (signature.startsWith('sha1=')) {
algorithm = 'sha1'
providedSignature = signature.substring(5)
} else {
logger.warn('GitHub signature has invalid format', {
signature: `${signature.substring(0, 10)}...`,
})
return false
}
const computedHash = crypto.createHmac(algorithm, secret).update(body, 'utf8').digest('hex')
logger.debug('GitHub signature comparison', {
algorithm,
computedSignature: `${computedHash.substring(0, 10)}...`,
providedSignature: `${providedSignature.substring(0, 10)}...`,
computedLength: computedHash.length,
providedLength: providedSignature.length,
match: computedHash === providedSignature,
})
return safeCompare(computedHash, providedSignature)
} catch (error) {
logger.error('Error validating GitHub signature:', error)
return false
}
}
/**
* Process Airtable payloads
*/
@@ -2773,48 +2365,3 @@ export function convertSquareBracketsToTwiML(twiml: string | undefined): string
// Replace [Tag] with <Tag> and [/Tag] with </Tag>
return twiml.replace(/\[(\/?[^\]]+)\]/g, '<$1>')
}
/**
* Validates a Cal.com webhook request signature using HMAC SHA-256
* @param secret - Cal.com webhook secret (plain text)
* @param signature - X-Cal-Signature-256 header value (hex-encoded HMAC SHA-256 signature)
* @param body - Raw request body string
* @returns Whether the signature is valid
*/
export function validateCalcomSignature(secret: string, signature: string, body: string): boolean {
try {
if (!secret || !signature || !body) {
logger.warn('Cal.com signature validation missing required fields', {
hasSecret: !!secret,
hasSignature: !!signature,
hasBody: !!body,
})
return false
}
// Cal.com sends signature in format: sha256=<hex>
// We need to strip the prefix before comparing
let providedSignature: string
if (signature.startsWith('sha256=')) {
providedSignature = signature.substring(7)
} else {
// If no prefix, use as-is (for backwards compatibility)
providedSignature = signature
}
const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex')
logger.debug('Cal.com signature comparison', {
computedSignature: `${computedHash.substring(0, 10)}...`,
providedSignature: `${providedSignature.substring(0, 10)}...`,
computedLength: computedHash.length,
providedLength: providedSignature.length,
match: computedHash === providedSignature,
})
return safeCompare(computedHash, providedSignature)
} catch (error) {
logger.error('Error validating Cal.com signature:', error)
return false
}
}