11import { createLogger } from '@sim/logger'
2- import { safeCompare } from '@sim/security/compare'
3- import { hmacSha256Base64 } from '@sim/security/hmac'
2+ import { verifyStandardWebhook } from '@sim/security/standard-webhooks'
43import { toRecordOrNull } from '@sim/utils/object'
54import { NextResponse } from 'next/server'
65import { getNotificationUrl , getProviderConfig } from '@/lib/webhooks/provider-subscription-utils'
@@ -20,51 +19,9 @@ const logger = createLogger('WebhookProvider:Granola')
2019
2120const GRANOLA_WEBHOOK_ENDPOINTS_URL = 'https://public-api.granola.ai/v1/webhook-endpoints'
2221
23- /** Granola advises rejecting deliveries whose timestamp is more than a few minutes old. */
24- const GRANOLA_WEBHOOK_TIMESTAMP_SKEW_SECONDS = 5 * 60
25-
2622/** Scopes used when the trigger leaves the field blank. */
2723const DEFAULT_GRANOLA_SCOPES = [ 'personal' , 'public' ]
2824
29- /**
30- * Verify a Granola webhook signature using the Standard Webhooks scheme:
31- * HMAC-SHA256 of `${webhook-id}.${webhook-timestamp}.${body}` keyed with the
32- * base64-decoded signing secret (after the `whsec_` prefix), compared against
33- * the `webhook-signature` header which may carry one or more space-separated
34- * `v1,<base64sig>` entries.
35- * @see https://docs.granola.ai/webhooks
36- */
37- function verifyGranolaSignature (
38- secret : string ,
39- msgId : string ,
40- timestamp : string ,
41- signatures : string ,
42- rawBody : string
43- ) : boolean {
44- try {
45- const ts = Number . parseInt ( timestamp , 10 )
46- const now = Math . floor ( Date . now ( ) / 1000 )
47- if ( Number . isNaN ( ts ) || Math . abs ( now - ts ) > GRANOLA_WEBHOOK_TIMESTAMP_SKEW_SECONDS ) {
48- return false
49- }
50-
51- const secretBytes = Buffer . from ( secret . replace ( / ^ w h s e c _ / , '' ) , 'base64' )
52- const expectedSignature = hmacSha256Base64 ( `${ msgId } .${ timestamp } .${ rawBody } ` , secretBytes )
53-
54- for ( const versionedSig of signatures . split ( ' ' ) ) {
55- const parts = versionedSig . split ( ',' )
56- if ( parts . length !== 2 ) continue
57- if ( parts [ 0 ] === 'v1' && safeCompare ( parts [ 1 ] , expectedSignature ) ) {
58- return true
59- }
60- }
61- return false
62- } catch ( error ) {
63- logger . error ( 'Error verifying Granola webhook signature:' , error )
64- return false
65- }
66- }
67-
6825/**
6926 * Parse a comma-separated or JSON-array trigger field into a string list.
7027 * Trigger fields are free text, so both forms are accepted.
@@ -187,19 +144,19 @@ export const granolaHandler: WebhookProviderHandler = {
187144 return new NextResponse ( 'Unauthorized - Granola signing secret is required' , { status : 401 } )
188145 }
189146
190- const webhookId = request . headers . get ( 'webhook-id' )
191- const webhookTimestamp = request . headers . get ( 'webhook-timestamp' )
192- const webhookSignature = request . headers . get ( 'webhook-signature' )
193-
194- if ( ! webhookId || ! webhookTimestamp || ! webhookSignature ) {
195- logger . warn ( `[ ${ requestId } ] Granola webhook missing Standard Webhooks signature headers` )
196- return new NextResponse ( 'Unauthorized - Missing Granola signature headers' , { status : 401 } )
197- }
198-
199- if (
200- ! verifyGranolaSignature ( signingSecret , webhookId , webhookTimestamp , webhookSignature , rawBody )
201- ) {
202- logger . warn ( `[ ${ requestId } ] Granola signature verification failed` )
147+ const verification = verifyStandardWebhook ( {
148+ secret : signingSecret ,
149+ rawBody ,
150+ headers : request . headers ,
151+ } )
152+ if ( ! verification . valid ) {
153+ if ( verification . reason === 'missing_headers' ) {
154+ logger . warn ( `[ ${ requestId } ] Granola webhook missing Standard Webhooks signature headers` )
155+ return new NextResponse ( 'Unauthorized - Missing Granola signature headers' , { status : 401 } )
156+ }
157+ logger . warn ( `[ ${ requestId } ] Granola signature verification failed` , {
158+ reason : verification . reason ,
159+ } )
203160 return new NextResponse ( 'Unauthorized - Invalid Granola signature' , { status : 401 } )
204161 }
205162
0 commit comments