@@ -2,11 +2,15 @@ import crypto from 'crypto'
22import { createLogger } from '@sim/logger'
33import { safeCompare } from '@sim/security/compare'
44import { NextResponse } from 'next/server'
5+ import { getNotificationUrl , getProviderConfig } from '@/lib/webhooks/provider-subscription-utils'
56import type {
67 AuthContext ,
8+ DeleteSubscriptionContext ,
79 EventMatchContext ,
810 FormatInputContext ,
911 FormatInputResult ,
12+ SubscriptionContext ,
13+ SubscriptionResult ,
1014 WebhookProviderHandler ,
1115} from '@/lib/webhooks/providers/types'
1216
@@ -16,6 +20,16 @@ function asRecord(value: unknown): Record<string, unknown> {
1620 return ( value as Record < string , unknown > ) || { }
1721}
1822
23+ /** Zendesk API base for a subdomain. */
24+ function zendeskApiBase ( subdomain : string ) : string {
25+ return `https://${ subdomain } .zendesk.com/api/v2`
26+ }
27+
28+ /** Basic auth header for the Zendesk API-token scheme (`email/token:apiToken`). */
29+ function zendeskAuthHeader ( email : string , apiToken : string ) : string {
30+ return `Basic ${ Buffer . from ( `${ email } /token:${ apiToken } ` ) . toString ( 'base64' ) } `
31+ }
32+
1933/** Maximum allowed clock skew (5 minutes) between Zendesk's signed timestamp and now, per Zendesk docs. */
2034const ZENDESK_TIMESTAMP_MAX_SKEW_MS = 5 * 60 * 1000
2135
@@ -130,4 +144,101 @@ export const zendeskHandler: WebhookProviderHandler = {
130144 extractIdempotencyId ( body : unknown ) {
131145 return ( asRecord ( body ) . id as string | undefined ) || null
132146 } ,
147+
148+ async createSubscription ( ctx : SubscriptionContext ) : Promise < SubscriptionResult | undefined > {
149+ const config = getProviderConfig ( ctx . webhook )
150+ const subdomain = config . subdomain as string | undefined
151+ const email = config . email as string | undefined
152+ const apiToken = config . apiToken as string | undefined
153+ const triggerId = config . triggerId as string | undefined
154+
155+ if ( ! subdomain ) throw new Error ( 'Zendesk subdomain is required to create the webhook.' )
156+ if ( ! email ) throw new Error ( 'Zendesk admin email is required to create the webhook.' )
157+ if ( ! apiToken ) throw new Error ( 'Zendesk API token is required to create the webhook.' )
158+
159+ const { getZendeskSubscriptions } = await import ( '@/triggers/zendesk/utils' )
160+ const apiBase = zendeskApiBase ( subdomain )
161+ const authHeader = zendeskAuthHeader ( email , apiToken )
162+
163+ const createRes = await fetch ( `${ apiBase } /webhooks` , {
164+ method : 'POST' ,
165+ headers : { Authorization : authHeader , 'Content-Type' : 'application/json' } ,
166+ body : JSON . stringify ( {
167+ webhook : {
168+ name : `Sim webhook (${ ctx . webhook . id } )` ,
169+ endpoint : getNotificationUrl ( ctx . webhook ) ,
170+ http_method : 'POST' ,
171+ request_format : 'json' ,
172+ status : 'active' ,
173+ subscriptions : getZendeskSubscriptions ( triggerId ?? 'zendesk_webhook' ) ,
174+ } ,
175+ } ) ,
176+ } )
177+
178+ if ( ! createRes . ok ) {
179+ const detail = await createRes . text ( ) . catch ( ( ) => '' )
180+ logger . error ( `[${ ctx . requestId } ] Failed to create Zendesk webhook (${ createRes . status } )` , {
181+ detail,
182+ } )
183+ if ( createRes . status === 401 || createRes . status === 403 ) {
184+ throw new Error (
185+ 'Zendesk authentication failed. Verify the subdomain, admin email, and API token.'
186+ )
187+ }
188+ throw new Error ( `Failed to create Zendesk webhook: ${ createRes . status } ` )
189+ }
190+
191+ const created = asRecord ( ( await createRes . json ( ) . catch ( ( ) => ( { } ) ) ) as unknown )
192+ const externalId = asRecord ( created . webhook ) . id as string | undefined
193+ if ( ! externalId ) throw new Error ( 'Zendesk webhook created but no webhook ID was returned.' )
194+
195+ const secretRes = await fetch ( `${ apiBase } /webhooks/${ externalId } /signing_secret` , {
196+ headers : { Authorization : authHeader } ,
197+ } )
198+ if ( ! secretRes . ok ) {
199+ const detail = await secretRes . text ( ) . catch ( ( ) => '' )
200+ logger . error (
201+ `[${ ctx . requestId } ] Created Zendesk webhook ${ externalId } but failed to fetch signing secret (${ secretRes . status } )` ,
202+ { detail }
203+ )
204+ throw new Error ( `Failed to fetch Zendesk signing secret: ${ secretRes . status } ` )
205+ }
206+
207+ const secretBody = asRecord ( ( await secretRes . json ( ) . catch ( ( ) => ( { } ) ) ) as unknown )
208+ const secret = asRecord ( secretBody . signing_secret ) . secret as string | undefined
209+ if ( ! secret ) throw new Error ( 'Zendesk did not return a signing secret for the webhook.' )
210+
211+ logger . info ( `[${ ctx . requestId } ] Created Zendesk webhook ${ externalId } ` )
212+ return { providerConfigUpdates : { externalId, webhookSecret : secret } }
213+ } ,
214+
215+ async deleteSubscription ( ctx : DeleteSubscriptionContext ) : Promise < void > {
216+ const config = getProviderConfig ( ctx . webhook )
217+ const subdomain = config . subdomain as string | undefined
218+ const email = config . email as string | undefined
219+ const apiToken = config . apiToken as string | undefined
220+ const externalId = config . externalId as string | undefined
221+
222+ if ( ! subdomain || ! email || ! apiToken || ! externalId ) {
223+ if ( ctx . strict ) throw new Error ( 'Missing Zendesk credentials or webhook ID for deletion.' )
224+ logger . warn (
225+ `[${ ctx . requestId } ] Skipping Zendesk webhook cleanup — missing credentials or webhook ID`
226+ )
227+ return
228+ }
229+
230+ const res = await fetch ( `${ zendeskApiBase ( subdomain ) } /webhooks/${ externalId } ` , {
231+ method : 'DELETE' ,
232+ headers : { Authorization : zendeskAuthHeader ( email , apiToken ) } ,
233+ } )
234+
235+ if ( ! res . ok && res . status !== 404 ) {
236+ if ( ctx . strict ) throw new Error ( `Failed to delete Zendesk webhook: ${ res . status } ` )
237+ logger . warn (
238+ `[${ ctx . requestId } ] Failed to delete Zendesk webhook ${ externalId } (non-fatal): ${ res . status } `
239+ )
240+ return
241+ }
242+ logger . info ( `[${ ctx . requestId } ] Deleted Zendesk webhook ${ externalId } ` )
243+ } ,
133244}
0 commit comments