mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-28 03:00:29 -04:00
refactor(security): consolidate HMAC-SHA256 primitives into @sim/security
Adds hmacSha256Hex and hmacSha256Base64 to @sim/security/hmac and migrates 15 webhook providers plus 5 other hot paths (deployment token signing, outbound webhook requests, workspace notification delivery, notification test route, Shopify OAuth callback) off bare `createHmac` calls. Secret parameter accepts `string | Buffer` to cover base64-decoded Svix-style secrets (Resend) and MS Teams' HMAC scheme. AWS SigV4 signing in S3 and Textract tools intentionally retains direct `createHmac` usage — its multi-step key derivation chain doesn't fit a generic helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import crypto from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { safeCompare } from '@sim/security/compare'
|
||||
import { hmacSha256Hex } from '@sim/security/hmac'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
@@ -35,7 +35,7 @@ function validateHmac(searchParams: URLSearchParams, clientSecret: string): bool
|
||||
.map((key) => `${key}=${params[key]}`)
|
||||
.join('&')
|
||||
|
||||
const generatedHmac = crypto.createHmac('sha256', clientSecret).update(message).digest('hex')
|
||||
const generatedHmac = hmacSha256Hex(message, clientSecret)
|
||||
|
||||
return safeCompare(hmac, generatedHmac)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createHmac } from 'crypto'
|
||||
import { db } from '@sim/db'
|
||||
import { account, workspaceNotificationSubscription } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { hmacSha256Hex } from '@sim/security/hmac'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
@@ -36,9 +36,7 @@ interface SlackConfig {
|
||||
|
||||
function generateSignature(secret: string, timestamp: number, body: string): string {
|
||||
const signatureBase = `${timestamp}.${body}`
|
||||
const hmac = createHmac('sha256', secret)
|
||||
hmac.update(signatureBase)
|
||||
return hmac.digest('hex')
|
||||
return hmacSha256Hex(signatureBase, secret)
|
||||
}
|
||||
|
||||
function buildTestPayload(subscription: typeof workspaceNotificationSubscription.$inferSelect) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { createHmac } from 'crypto'
|
||||
import { db, workflowExecutionLogs } from '@sim/db'
|
||||
import {
|
||||
account,
|
||||
@@ -6,6 +5,7 @@ import {
|
||||
workspaceNotificationSubscription,
|
||||
} from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { hmacSha256Hex } from '@sim/security/hmac'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { formatDuration } from '@sim/utils/formatting'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
@@ -62,9 +62,7 @@ interface NotificationPayload {
|
||||
|
||||
function generateSignature(secret: string, timestamp: number, body: string): string {
|
||||
const signatureBase = `${timestamp}.${body}`
|
||||
const hmac = createHmac('sha256', secret)
|
||||
hmac.update(signatureBase)
|
||||
return hmac.digest('hex')
|
||||
return hmacSha256Hex(signatureBase, secret)
|
||||
}
|
||||
|
||||
async function buildPayload(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createHmac } from 'crypto'
|
||||
import { safeCompare } from '@sim/security/compare'
|
||||
import { sha256Hex } from '@sim/security/hash'
|
||||
import { hmacSha256Hex } from '@sim/security/hmac'
|
||||
import type { NextRequest, NextResponse } from 'next/server'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
import { isDev } from '@/lib/core/config/feature-flags'
|
||||
@@ -11,7 +11,7 @@ import { isDev } from '@/lib/core/config/feature-flags'
|
||||
*/
|
||||
|
||||
function signPayload(payload: string): string {
|
||||
return createHmac('sha256', env.BETTER_AUTH_SECRET).update(payload).digest('hex')
|
||||
return hmacSha256Hex(payload, env.BETTER_AUTH_SECRET)
|
||||
}
|
||||
|
||||
function passwordSlot(encryptedPassword?: string | null): string {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import crypto from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { safeCompare } from '@sim/security/compare'
|
||||
import { hmacSha256Hex } from '@sim/security/hmac'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils'
|
||||
import type {
|
||||
@@ -24,7 +24,7 @@ function validateAshbySignature(secretToken: string, signature: string, body: st
|
||||
return false
|
||||
}
|
||||
const providedSignature = signature.substring(7)
|
||||
const computedHash = crypto.createHmac('sha256', secretToken).update(body, 'utf8').digest('hex')
|
||||
const computedHash = hmacSha256Hex(body, secretToken)
|
||||
return safeCompare(computedHash, providedSignature)
|
||||
} catch (error) {
|
||||
logger.error('Error validating Ashby signature:', error)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import crypto from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { safeCompare } from '@sim/security/compare'
|
||||
import { hmacSha256Hex } from '@sim/security/hmac'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
@@ -29,7 +29,7 @@ function validateAttioSignature(secret: string, signature: string, body: string)
|
||||
})
|
||||
return false
|
||||
}
|
||||
const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex')
|
||||
const computedHash = hmacSha256Hex(body, secret)
|
||||
logger.debug('Attio signature comparison', {
|
||||
computedSignature: `${computedHash.substring(0, 10)}...`,
|
||||
providedSignature: `${signature.substring(0, 10)}...`,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import crypto from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { safeCompare } from '@sim/security/compare'
|
||||
import { hmacSha256Hex } from '@sim/security/hmac'
|
||||
import type { WebhookProviderHandler } from '@/lib/webhooks/providers/types'
|
||||
import { createHmacVerifier } from '@/lib/webhooks/providers/utils'
|
||||
|
||||
@@ -22,7 +22,7 @@ function validateCalcomSignature(secret: string, signature: string, body: string
|
||||
} else {
|
||||
providedSignature = signature
|
||||
}
|
||||
const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex')
|
||||
const computedHash = hmacSha256Hex(body, secret)
|
||||
logger.debug('Cal.com signature comparison', {
|
||||
computedSignature: `${computedHash.substring(0, 10)}...`,
|
||||
providedSignature: `${providedSignature.substring(0, 10)}...`,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import crypto from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { safeCompare } from '@sim/security/compare'
|
||||
import { hmacSha256Hex } from '@sim/security/hmac'
|
||||
import type {
|
||||
FormatInputContext,
|
||||
FormatInputResult,
|
||||
@@ -20,7 +20,7 @@ function validateCirclebackSignature(secret: string, signature: string, body: st
|
||||
})
|
||||
return false
|
||||
}
|
||||
const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex')
|
||||
const computedHash = hmacSha256Hex(body, secret)
|
||||
logger.debug('Circleback signature comparison', {
|
||||
computedSignature: `${computedHash.substring(0, 10)}...`,
|
||||
providedSignature: `${signature.substring(0, 10)}...`,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import crypto from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { safeCompare } from '@sim/security/compare'
|
||||
import { hmacSha256Hex } from '@sim/security/hmac'
|
||||
import type {
|
||||
FormatInputContext,
|
||||
FormatInputResult,
|
||||
@@ -27,7 +27,7 @@ function validateFirefliesSignature(secret: string, signature: string, body: str
|
||||
return false
|
||||
}
|
||||
const providedSignature = signature.substring(7)
|
||||
const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex')
|
||||
const computedHash = hmacSha256Hex(body, secret)
|
||||
logger.debug('Fireflies signature comparison', {
|
||||
computedSignature: `${computedHash.substring(0, 10)}...`,
|
||||
providedSignature: `${providedSignature.substring(0, 10)}...`,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import crypto from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { safeCompare } from '@sim/security/compare'
|
||||
import { hmacSha256Hex } from '@sim/security/hmac'
|
||||
import type {
|
||||
EventMatchContext,
|
||||
FormatInputContext,
|
||||
@@ -25,7 +25,7 @@ function validateGreenhouseSignature(secretKey: string, signature: string, body:
|
||||
return false
|
||||
}
|
||||
const providedDigest = signature.substring(prefix.length)
|
||||
const computedDigest = crypto.createHmac('sha256', secretKey).update(body, 'utf8').digest('hex')
|
||||
const computedDigest = hmacSha256Hex(body, secretKey)
|
||||
return safeCompare(computedDigest, providedDigest)
|
||||
} catch {
|
||||
logger.error('Error validating Greenhouse signature')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import crypto from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { safeCompare } from '@sim/security/compare'
|
||||
import { hmacSha256Hex } from '@sim/security/hmac'
|
||||
import type {
|
||||
EventMatchContext,
|
||||
FormatInputContext,
|
||||
@@ -28,7 +28,7 @@ export function validateJiraSignature(secret: string, signature: string, body: s
|
||||
return false
|
||||
}
|
||||
const providedSignature = signature.substring(7)
|
||||
const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex')
|
||||
const computedHash = hmacSha256Hex(body, secret)
|
||||
logger.debug('Jira signature comparison', {
|
||||
computedLength: computedHash.length,
|
||||
providedLength: providedSignature.length,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import crypto from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { safeCompare } from '@sim/security/compare'
|
||||
import { hmacSha256Hex } from '@sim/security/hmac'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { NextResponse } from 'next/server'
|
||||
@@ -28,7 +28,7 @@ function validateLinearSignature(secret: string, signature: string, body: string
|
||||
})
|
||||
return false
|
||||
}
|
||||
const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex')
|
||||
const computedHash = hmacSha256Hex(body, secret)
|
||||
logger.debug('Linear signature comparison', {
|
||||
computedSignature: `${computedHash.substring(0, 10)}...`,
|
||||
providedSignature: `${signature.substring(0, 10)}...`,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import crypto from 'crypto'
|
||||
import { db } from '@sim/db'
|
||||
import { account } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { safeCompare } from '@sim/security/compare'
|
||||
import { hmacSha256Base64 } from '@sim/security/hmac'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
@@ -46,8 +46,7 @@ function validateMicrosoftTeamsSignature(
|
||||
}
|
||||
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')
|
||||
const computedHash = hmacSha256Base64(body, secretBytes)
|
||||
return safeCompare(computedHash, providedSignature)
|
||||
} catch (error) {
|
||||
logger.error('Error validating Microsoft Teams signature:', error)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import crypto from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { safeCompare } from '@sim/security/compare'
|
||||
import { hmacSha256Hex } from '@sim/security/hmac'
|
||||
import { NextResponse } from 'next/server'
|
||||
import type {
|
||||
EventMatchContext,
|
||||
@@ -28,7 +28,7 @@ function validateNotionSignature(secret: string, signature: string, body: string
|
||||
}
|
||||
|
||||
const providedHash = signature.startsWith('sha256=') ? signature.slice(7) : signature
|
||||
const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex')
|
||||
const computedHash = hmacSha256Hex(body, secret)
|
||||
|
||||
logger.debug('Notion signature comparison', {
|
||||
computedSignature: `${computedHash.substring(0, 10)}...`,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import crypto from 'node:crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { safeCompare } from '@sim/security/compare'
|
||||
import { hmacSha256Base64 } from '@sim/security/hmac'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils'
|
||||
import type {
|
||||
@@ -41,10 +41,7 @@ function verifySvixSignature(
|
||||
|
||||
const secretBytes = Buffer.from(secret.replace(/^whsec_/, ''), 'base64')
|
||||
const toSign = `${msgId}.${timestamp}.${rawBody}`
|
||||
const expectedSignature = crypto
|
||||
.createHmac('sha256', secretBytes)
|
||||
.update(toSign, 'utf8')
|
||||
.digest('base64')
|
||||
const expectedSignature = hmacSha256Base64(toSign, secretBytes)
|
||||
|
||||
const providedSignatures = signatures.split(' ')
|
||||
for (const versionedSig of providedSignatures) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import crypto from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { safeCompare } from '@sim/security/compare'
|
||||
import { hmacSha256Hex } from '@sim/security/hmac'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { NextResponse } from 'next/server'
|
||||
import {
|
||||
@@ -207,10 +207,7 @@ function validateSlackSignature(
|
||||
|
||||
const providedSignature = signature.substring(3)
|
||||
const basestring = `v0:${timestamp}:${rawBody}`
|
||||
const computedHash = crypto
|
||||
.createHmac('sha256', signingSecret)
|
||||
.update(basestring, 'utf8')
|
||||
.digest('hex')
|
||||
const computedHash = hmacSha256Hex(basestring, signingSecret)
|
||||
|
||||
return safeCompare(computedHash, providedSignature)
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import crypto from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { safeCompare } from '@sim/security/compare'
|
||||
import { hmacSha256Base64 } from '@sim/security/hmac'
|
||||
import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils'
|
||||
import type {
|
||||
DeleteSubscriptionContext,
|
||||
@@ -23,7 +23,7 @@ function validateTypeformSignature(secret: string, signature: string, body: stri
|
||||
return false
|
||||
}
|
||||
const providedSignature = signature.substring(7)
|
||||
const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('base64')
|
||||
const computedHash = hmacSha256Base64(body, secret)
|
||||
return safeCompare(computedHash, providedSignature)
|
||||
} catch (error) {
|
||||
logger.error('Error validating Typeform signature:', error)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { createHmac } from 'crypto'
|
||||
import { db, workflowDeploymentVersion } from '@sim/db'
|
||||
import { webhook } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { safeCompare } from '@sim/security/compare'
|
||||
import { sha256Hex } from '@sim/security/hash'
|
||||
import { hmacSha256Hex } from '@sim/security/hmac'
|
||||
import { and, eq, isNull, or } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import type {
|
||||
@@ -111,7 +111,7 @@ function validateWhatsAppSignature(secret: string, signature: string, body: stri
|
||||
}
|
||||
|
||||
const providedSignature = signature.substring(7)
|
||||
const computedSignature = createHmac('sha256', secret).update(body, 'utf8').digest('hex')
|
||||
const computedSignature = hmacSha256Hex(body, secret)
|
||||
|
||||
return safeCompare(computedSignature, providedSignature)
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import crypto from 'crypto'
|
||||
import { db, webhook, workflow } from '@sim/db'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { safeCompare } from '@sim/security/compare'
|
||||
import { hmacSha256Hex } from '@sim/security/hmac'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
@@ -41,7 +41,7 @@ export function validateZoomSignature(
|
||||
}
|
||||
|
||||
const message = `v0:${timestamp}:${body}`
|
||||
const computedHash = crypto.createHmac('sha256', secretToken).update(message).digest('hex')
|
||||
const computedHash = hmacSha256Hex(message, secretToken)
|
||||
const expectedSignature = `v0=${computedHash}`
|
||||
|
||||
return safeCompare(expectedSignature, signature)
|
||||
@@ -206,10 +206,7 @@ export const zoomHandler: WebhookProviderHandler = {
|
||||
secretToken &&
|
||||
validateZoomSignature(secretToken, signature, timestamp, bodyForSignature)
|
||||
) {
|
||||
const hashForValidate = crypto
|
||||
.createHmac('sha256', secretToken)
|
||||
.update(plainToken)
|
||||
.digest('hex')
|
||||
const hashForValidate = hmacSha256Hex(plainToken, secretToken)
|
||||
|
||||
return NextResponse.json({
|
||||
plainToken,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createHmac } from 'crypto'
|
||||
import { hmacSha256Hex } from '@sim/security/hmac'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import type { RequestResponse, WebhookRequestParams } from '@/tools/http/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
@@ -8,7 +8,7 @@ import type { ToolConfig } from '@/tools/types'
|
||||
*/
|
||||
function generateSignature(secret: string, timestamp: number, body: string): string {
|
||||
const signatureBase = `${timestamp}.${body}`
|
||||
return createHmac('sha256', secret).update(signatureBase).digest('hex')
|
||||
return hmacSha256Hex(signatureBase, secret)
|
||||
}
|
||||
|
||||
export const webhookRequestTool: ToolConfig<WebhookRequestParams, RequestResponse> = {
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
"types": "./src/hash.ts",
|
||||
"default": "./src/hash.ts"
|
||||
},
|
||||
"./hmac": {
|
||||
"types": "./src/hmac.ts",
|
||||
"default": "./src/hmac.ts"
|
||||
},
|
||||
"./tokens": {
|
||||
"types": "./src/tokens.ts",
|
||||
"default": "./src/tokens.ts"
|
||||
|
||||
43
packages/security/src/hmac.test.ts
Normal file
43
packages/security/src/hmac.test.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { hmacSha256Base64, hmacSha256Hex } from './hmac'
|
||||
|
||||
describe('hmacSha256Hex', () => {
|
||||
it('is deterministic', () => {
|
||||
expect(hmacSha256Hex('body', 'secret')).toBe(hmacSha256Hex('body', 'secret'))
|
||||
})
|
||||
|
||||
it('returns a 64-char hex digest', () => {
|
||||
expect(hmacSha256Hex('body', 'secret')).toMatch(/^[0-9a-f]{64}$/)
|
||||
})
|
||||
|
||||
it('matches RFC 4231 test vector 1', () => {
|
||||
const key = Buffer.from('0b'.repeat(20), 'hex').toString('binary')
|
||||
expect(hmacSha256Hex('Hi There', key)).toBe(
|
||||
'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7'
|
||||
)
|
||||
})
|
||||
|
||||
it('differs when body changes', () => {
|
||||
expect(hmacSha256Hex('a', 'k')).not.toBe(hmacSha256Hex('b', 'k'))
|
||||
})
|
||||
|
||||
it('differs when secret changes', () => {
|
||||
expect(hmacSha256Hex('body', 'k1')).not.toBe(hmacSha256Hex('body', 'k2'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('hmacSha256Base64', () => {
|
||||
it('is deterministic', () => {
|
||||
expect(hmacSha256Base64('body', 'secret')).toBe(hmacSha256Base64('body', 'secret'))
|
||||
})
|
||||
|
||||
it('returns a base64 digest', () => {
|
||||
expect(hmacSha256Base64('body', 'secret')).toMatch(/^[A-Za-z0-9+/]+=*$/)
|
||||
})
|
||||
|
||||
it('agrees with hex form via Buffer conversion', () => {
|
||||
const hex = hmacSha256Hex('body', 'secret')
|
||||
const b64 = hmacSha256Base64('body', 'secret')
|
||||
expect(Buffer.from(b64, 'base64').toString('hex')).toBe(hex)
|
||||
})
|
||||
})
|
||||
24
packages/security/src/hmac.ts
Normal file
24
packages/security/src/hmac.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { createHmac } from 'node:crypto'
|
||||
|
||||
/**
|
||||
* HMAC-SHA256 of a UTF-8 body using the given secret, hex-encoded. Use for
|
||||
* webhook signature verification where the provider sends a hex digest
|
||||
* (e.g. `X-Signature: <hex>` or `X-Hub-Signature-256: sha256=<hex>`). Pass the
|
||||
* secret as a `Buffer` when the provider's scheme requires base64-decoding
|
||||
* (e.g. Svix-compatible `whsec_...` secrets). Pair with
|
||||
* {@link ../compare | safeCompare} for timing-safe comparison.
|
||||
*/
|
||||
export function hmacSha256Hex(body: string, secret: string | Buffer): string {
|
||||
return createHmac('sha256', secret).update(body, 'utf8').digest('hex')
|
||||
}
|
||||
|
||||
/**
|
||||
* HMAC-SHA256 of a UTF-8 body using the given secret, base64-encoded. Use for
|
||||
* webhook signature verification where the provider sends a base64 digest
|
||||
* (e.g. Typeform, Microsoft Teams outgoing webhooks). Pass the secret as a
|
||||
* `Buffer` when the provider's scheme requires base64-decoding. Pair with
|
||||
* {@link ../compare | safeCompare} for timing-safe comparison.
|
||||
*/
|
||||
export function hmacSha256Base64(body: string, secret: string | Buffer): string {
|
||||
return createHmac('sha256', secret).update(body, 'utf8').digest('base64')
|
||||
}
|
||||
Reference in New Issue
Block a user