Skip to content

Commit 6e95981

Browse files
committed
refactor(webhooks): extract provider-specific logic into handler registry
1 parent ebc1948 commit 6e95981

36 files changed

+1552
-1323
lines changed

.claude/commands/add-trigger.md

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,115 @@ All fields automatically have:
552552
- `mode: 'trigger'` - Only shown in trigger mode
553553
- `condition: { field: 'selectedTriggerId', value: triggerId }` - Only shown when this trigger is selected
554554

555+
## Webhook Provider Handler (Optional)
556+
557+
If the service requires **custom webhook auth** (HMAC signatures, token validation), **event matching** (filtering by trigger type), or **idempotency dedup**, create a provider handler in the webhook provider registry.
558+
559+
### Directory
560+
561+
```
562+
apps/sim/lib/webhooks/providers/
563+
├── types.ts # WebhookProviderHandler interface
564+
├── utils.ts # Shared helpers (createHmacVerifier, verifyTokenAuth, skipByEventTypes)
565+
├── registry.ts # Handler map + default handler
566+
├── index.ts # Barrel export
567+
└── {service}.ts # Your provider handler
568+
```
569+
570+
### When to Create a Handler
571+
572+
| Behavior | Method to implement | Example providers |
573+
|---|---|---|
574+
| HMAC signature auth | `verifyAuth` via `createHmacVerifier` | Ashby, Jira, Linear, Typeform |
575+
| Custom token auth | `verifyAuth` via `verifyTokenAuth` | Generic, Google Forms |
576+
| Event type filtering | `matchEvent` | GitHub, Jira, Confluence, Attio, HubSpot |
577+
| Event skip by type list | `shouldSkipEvent` via `skipByEventTypes` | Stripe, Grain |
578+
| Idempotency dedup | `extractIdempotencyId` | Slack, Stripe, Linear, Jira |
579+
| Custom success response | `formatSuccessResponse` | Slack, Twilio Voice, Microsoft Teams |
580+
| Custom error format | `formatErrorResponse` | Microsoft Teams |
581+
582+
If none of these apply, you do NOT need a handler file. The default handler provides bearer token auth for providers that set `providerConfig.token`.
583+
584+
### Simple Example: HMAC Auth Only
585+
586+
```typescript
587+
import type { WebhookProviderHandler } from '@/lib/webhooks/providers/types'
588+
import { createHmacVerifier } from '@/lib/webhooks/providers/utils'
589+
import { validate{Service}Signature } from '@/lib/webhooks/utils.server'
590+
591+
export const {service}Handler: WebhookProviderHandler = {
592+
verifyAuth: createHmacVerifier({
593+
configKey: 'webhookSecret',
594+
headerName: 'X-{Service}-Signature',
595+
validateFn: validate{Service}Signature,
596+
providerLabel: '{Service}',
597+
}),
598+
}
599+
```
600+
601+
### Example: Auth + Event Matching + Idempotency
602+
603+
```typescript
604+
import { createLogger } from '@sim/logger'
605+
import type { EventMatchContext, WebhookProviderHandler } from '@/lib/webhooks/providers/types'
606+
import { createHmacVerifier } from '@/lib/webhooks/providers/utils'
607+
import { validate{Service}Signature } from '@/lib/webhooks/utils.server'
608+
609+
const logger = createLogger('WebhookProvider:{Service}')
610+
611+
export const {service}Handler: WebhookProviderHandler = {
612+
verifyAuth: createHmacVerifier({
613+
configKey: 'webhookSecret',
614+
headerName: 'X-{Service}-Signature',
615+
validateFn: validate{Service}Signature,
616+
providerLabel: '{Service}',
617+
}),
618+
619+
async matchEvent({ webhook, workflow, body, requestId, providerConfig }: EventMatchContext) {
620+
const triggerId = providerConfig.triggerId as string | undefined
621+
const obj = body as Record<string, unknown>
622+
623+
if (triggerId && triggerId !== '{service}_webhook') {
624+
const { is{Service}EventMatch } = await import('@/triggers/{service}/utils')
625+
if (!is{Service}EventMatch(triggerId, obj)) {
626+
logger.debug(
627+
`[${requestId}] {Service} event mismatch for trigger ${triggerId}. Skipping.`,
628+
{ webhookId: webhook.id, workflowId: workflow.id, triggerId }
629+
)
630+
return false
631+
}
632+
}
633+
634+
return true
635+
},
636+
637+
extractIdempotencyId(body: unknown) {
638+
const obj = body as Record<string, unknown>
639+
if (obj.id && obj.type) {
640+
return `${obj.type}:${obj.id}`
641+
}
642+
return null
643+
},
644+
}
645+
```
646+
647+
### Registering the Handler
648+
649+
In `apps/sim/lib/webhooks/providers/registry.ts`:
650+
651+
```typescript
652+
import { {service}Handler } from '@/lib/webhooks/providers/{service}'
653+
654+
const PROVIDER_HANDLERS: Record<string, WebhookProviderHandler> = {
655+
// ... existing providers (alphabetical) ...
656+
{service}: {service}Handler,
657+
}
658+
```
659+
660+
### Adding a Signature Validator
661+
662+
If the service uses HMAC signatures, add a `validate{Service}Signature` function in `apps/sim/lib/webhooks/utils.server.ts` alongside the existing validators. Then reference it from your handler via `createHmacVerifier`.
663+
555664
## Trigger Outputs & Webhook Input Formatting
556665

557666
### Important: Two Sources of Truth
@@ -696,6 +805,14 @@ export const {service}WebhookTrigger: TriggerConfig = {
696805
- [ ] Added `delete{Service}Webhook` function to `provider-subscriptions.ts`
697806
- [ ] Added provider to `cleanupExternalWebhook` function
698807

808+
### Webhook Provider Handler (if needed)
809+
- [ ] Created `apps/sim/lib/webhooks/providers/{service}.ts` handler file
810+
- [ ] Registered handler in `apps/sim/lib/webhooks/providers/registry.ts` (alphabetical)
811+
- [ ] Used `createHmacVerifier` from `providers/utils` for HMAC-based auth
812+
- [ ] Used `verifyTokenAuth` from `providers/utils` for token-based auth
813+
- [ ] Added `validate{Service}Signature` in `utils.server.ts` (if HMAC auth needed)
814+
- [ ] Event matching uses dynamic `await import()` for trigger utils
815+
699816
### Webhook Input Formatting
700817
- [ ] Added handler in `apps/sim/lib/webhooks/utils.server.ts` (if custom formatting needed)
701818
- [ ] Handler returns fields matching trigger `outputs` exactly

apps/sim/app/api/webhooks/trigger/[path]/route.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,6 @@ vi.mock('@/background/logs-webhook-delivery', () => ({
156156
vi.mock('@/lib/webhooks/utils', () => ({
157157
handleWhatsAppVerification: handleWhatsAppVerificationMock,
158158
handleSlackChallenge: handleSlackChallengeMock,
159-
verifyProviderWebhook: vi.fn().mockReturnValue(null),
160159
processWhatsAppDeduplication: processWhatsAppDeduplicationMock,
161160
processGenericDeduplication: processGenericDeduplicationMock,
162161
fetchAndProcessAirtablePayloads: fetchAndProcessAirtablePayloadsMock,

apps/sim/app/api/webhooks/trigger/[path]/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ async function handleWebhookPost(
8787
if (webhooksForPath.length === 0) {
8888
const verificationResponse = await handlePreLookupWebhookVerification(
8989
request.method,
90-
body,
90+
body as Record<string, unknown> | undefined,
9191
requestId,
9292
path
9393
)

0 commit comments

Comments
 (0)