Skip to content

Commit cac054a

Browse files
committed
add generic webhook test event verification subblock
1 parent 90b7193 commit cac054a

File tree

6 files changed

+105
-7
lines changed

6 files changed

+105
-7
lines changed

apps/sim/lib/webhooks/deploy.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,7 @@ export async function saveTriggerWebhooksForDeploy({
602602
provider,
603603
workflowId,
604604
blockId: block.id,
605+
metadata: providerConfig,
605606
})
606607

607608
const result = await createExternalWebhookSubscription(

apps/sim/lib/webhooks/pending-verification.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,44 @@ describe('pending webhook verification', () => {
7272
).toBe(false)
7373
})
7474

75+
it('does not register generic pending verification unless verifyTestEvents is enabled', async () => {
76+
await registerPendingWebhookVerification({
77+
path: 'grain-path-3',
78+
provider: 'generic',
79+
metadata: { verifyTestEvents: false },
80+
})
81+
82+
expect(await getPendingWebhookVerification('grain-path-3')).toBeNull()
83+
})
84+
85+
it('registers generic pending verification when verifyTestEvents is enabled', async () => {
86+
await registerPendingWebhookVerification({
87+
path: 'grain-path-3',
88+
provider: 'generic',
89+
metadata: { verifyTestEvents: true },
90+
})
91+
92+
const entry = await getPendingWebhookVerification('grain-path-3')
93+
94+
expect(entry).toMatchObject({
95+
path: 'grain-path-3',
96+
provider: 'generic',
97+
metadata: { verifyTestEvents: true },
98+
})
99+
expect(
100+
matchesPendingWebhookVerificationProbe(entry!, {
101+
method: 'POST',
102+
body: {},
103+
})
104+
).toBe(true)
105+
expect(
106+
matchesPendingWebhookVerificationProbe(entry!, {
107+
method: 'POST',
108+
body: { message: 'real event' },
109+
})
110+
).toBe(false)
111+
})
112+
75113
it('clears tracked pending verifications after a successful lifecycle', async () => {
76114
const tracker = new PendingWebhookVerificationTracker()
77115

apps/sim/lib/webhooks/pending-verification.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,23 @@ interface PendingWebhookVerificationProbe {
3131
body: Record<string, unknown> | undefined
3232
}
3333

34+
type PendingWebhookVerificationRegistrationMatcher = (
35+
registration: PendingWebhookVerificationRegistration
36+
) => boolean
37+
3438
type PendingWebhookVerificationProbeMatcher = (
3539
probe: PendingWebhookVerificationProbe,
3640
entry: PendingWebhookVerification
3741
) => boolean
3842

43+
const pendingWebhookVerificationRegistrationMatchers: Record<
44+
string,
45+
PendingWebhookVerificationRegistrationMatcher
46+
> = {
47+
grain: () => true,
48+
generic: (registration) => registration.metadata?.verifyTestEvents === true,
49+
}
50+
3951
const pendingWebhookVerificationProbeMatchers: Record<
4052
string,
4153
PendingWebhookVerificationProbeMatcher
@@ -44,6 +56,10 @@ const pendingWebhookVerificationProbeMatchers: Record<
4456
method === 'GET' ||
4557
method === 'HEAD' ||
4658
(method === 'POST' && (!body || Object.keys(body).length === 0 || !body.type)),
59+
generic: ({ method, body }) =>
60+
method === 'GET' ||
61+
method === 'HEAD' ||
62+
(method === 'POST' && (!body || Object.keys(body).length === 0)),
4763
}
4864

4965
function getRedisKey(path: string): string {
@@ -68,14 +84,27 @@ function getInMemoryPendingWebhookVerification(path: string): PendingWebhookVeri
6884
return entry
6985
}
7086

71-
export function requiresPendingWebhookVerification(provider: string): boolean {
72-
return provider in pendingWebhookVerificationProbeMatchers
87+
export function requiresPendingWebhookVerification(
88+
provider: string,
89+
metadata?: Record<string, unknown>
90+
): boolean {
91+
const registrationMatcher = pendingWebhookVerificationRegistrationMatchers[provider]
92+
if (!registrationMatcher) {
93+
return false
94+
}
95+
96+
return registrationMatcher({
97+
path: '',
98+
provider,
99+
metadata,
100+
})
73101
}
74102

75103
export async function registerPendingWebhookVerification(
76104
registration: PendingWebhookVerificationRegistration
77105
): Promise<void> {
78-
if (!requiresPendingWebhookVerification(registration.provider)) {
106+
const registrationMatcher = pendingWebhookVerificationRegistrationMatchers[registration.provider]
107+
if (!registrationMatcher || !registrationMatcher(registration)) {
79108
return
80109
}
81110

@@ -160,7 +189,9 @@ export class PendingWebhookVerificationTracker {
160189
private readonly registeredPaths = new Set<string>()
161190

162191
async register(registration: PendingWebhookVerificationRegistration): Promise<void> {
163-
if (!requiresPendingWebhookVerification(registration.provider)) {
192+
const registrationMatcher =
193+
pendingWebhookVerificationRegistrationMatchers[registration.provider]
194+
if (!registrationMatcher || !registrationMatcher(registration)) {
164195
return
165196
}
166197

apps/sim/lib/webhooks/provider-subscriptions.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1343,15 +1343,29 @@ export async function createGrainWebhookSubscription(
13431343
throw new Error(userFriendlyMessage)
13441344
}
13451345

1346+
const grainWebhookId = responseBody.id
1347+
1348+
if (!grainWebhookId) {
1349+
grainLogger.error(
1350+
`[${requestId}] Grain webhook creation response missing id for webhook ${webhookData.id}.`,
1351+
{
1352+
response: responseBody,
1353+
}
1354+
)
1355+
throw new Error(
1356+
'Grain webhook created but no webhook ID was returned in the response. Cannot track subscription.'
1357+
)
1358+
}
1359+
13461360
grainLogger.info(
13471361
`[${requestId}] Successfully created webhook in Grain for webhook ${webhookData.id}.`,
13481362
{
1349-
grainWebhookId: responseBody.id ?? responseBody.hook?.id,
1363+
grainWebhookId,
13501364
eventTypes,
13511365
}
13521366
)
13531367

1354-
return { id: responseBody.id ?? responseBody.hook?.id, eventTypes }
1368+
return { id: grainWebhookId, eventTypes }
13551369
} catch (error: any) {
13561370
grainLogger.error(
13571371
`[${requestId}] Exception during Grain webhook creation for webhook ${webhookData.id}.`,

apps/sim/tools/grain/create_hook.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,13 @@ export const grainCreateHookTool: ToolConfig<GrainCreateHookParams, GrainCreateH
6565
throw new Error(data.error || data.message || 'Failed to create webhook')
6666
}
6767

68+
if (!data?.id) {
69+
throw new Error('Grain webhook created but response did not include a webhook id')
70+
}
71+
6872
return {
6973
success: true,
70-
output: data.hook || data,
74+
output: data,
7175
}
7276
},
7377

apps/sim/triggers/generic/webhook.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,15 @@ export const genericWebhookTrigger: TriggerConfig = {
7070
defaultValue: 'default',
7171
mode: 'trigger',
7272
},
73+
{
74+
id: 'verifyTestEvents',
75+
title: 'Verify Test Events',
76+
type: 'switch',
77+
description:
78+
'Return a temporary 200 response for test or verification probes on this webhook URL during setup.',
79+
defaultValue: false,
80+
mode: 'trigger',
81+
},
7382
{
7483
id: 'responseStatusCode',
7584
title: 'Response Status Code',
@@ -120,6 +129,7 @@ export const genericWebhookTrigger: TriggerConfig = {
120129
'All request data (headers, body, query parameters) will be available in your workflow.',
121130
'If authentication is enabled, include the token in requests using either the custom header or "Authorization: Bearer TOKEN".',
122131
'To deduplicate incoming events, set the Deduplication Field to the dot-notation path of a unique identifier in the payload (e.g. "event.id"). Duplicate values within 7 days will be skipped.',
132+
'Enable "Verify Test Events" only if the sending service needs a temporary 200 response while validating the webhook URL.',
123133
]
124134
.map(
125135
(instruction, index) =>

0 commit comments

Comments
 (0)