Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion apps/sim/app/api/webhooks/trigger/[path]/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ vi.mock('@/lib/core/utils/request', () => requestUtilsMock)

process.env.DATABASE_URL = 'postgresql://test:test@localhost:5432/test'

import { POST } from '@/app/api/webhooks/trigger/[path]/route'
import { GET, POST } from '@/app/api/webhooks/trigger/[path]/route'

describe('Webhook Trigger API Route', () => {
beforeEach(() => {
Expand Down Expand Up @@ -401,6 +401,44 @@ describe('Webhook Trigger API Route', () => {
expect(text).toMatch(/not found/i)
})

it('should return 200 for GET verification probes before webhook persistence', async () => {
const req = createMockRequest(
'GET',
undefined,
{},
'http://localhost:3000/api/webhooks/trigger/non-existent-path'
)

const params = Promise.resolve({ path: 'non-existent-path' })

const response = await GET(req as any, { params })

expect(response.status).toBe(200)
await expect(response.json()).resolves.toMatchObject({
status: 'ok',
message: 'Webhook endpoint verified',
})
})

it('should return 200 for empty POST verification probes before webhook persistence', async () => {
const req = createMockRequest(
'POST',
undefined,
{},
'http://localhost:3000/api/webhooks/trigger/non-existent-path'
)

const params = Promise.resolve({ path: 'non-existent-path' })

const response = await POST(req as any, { params })

expect(response.status).toBe(200)
await expect(response.json()).resolves.toMatchObject({
status: 'ok',
message: 'Webhook endpoint verified',
})
})

describe('Generic Webhook Authentication', () => {
it('should process generic webhook without authentication', async () => {
testData.webhooks.push({
Expand Down
18 changes: 17 additions & 1 deletion apps/sim/app/api/webhooks/trigger/[path]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
checkWebhookPreprocessing,
findAllWebhooksForPath,
handlePreDeploymentVerification,
handlePreLookupWebhookVerification,
handleProviderChallenges,
handleProviderReachabilityTest,
parseWebhookBody,
Expand All @@ -30,7 +31,12 @@
return challengeResponse
}

return new NextResponse('Method not allowed', { status: 405 })
return handlePreLookupWebhookVerification(

Check failure on line 34 in apps/sim/app/api/webhooks/trigger/[path]/route.ts

View workflow job for this annotation

GitHub Actions / Test and Build / Test and Build

app/api/webhooks/trigger/[path]/route.test.ts > Webhook Trigger API Route > should return 200 for GET verification probes before webhook persistence

Error: [vitest] No "handlePreLookupWebhookVerification" export is defined on the "@/lib/webhooks/processor" mock. Did you forget to return it from "vi.mock"? If you need to partially mock a module, you can use "importOriginal" helper inside: vi.mock(import("@/lib/webhooks/processor"), async (importOriginal) => { const actual = await importOriginal() return { ...actual, // your mocked methods } }) ❯ Module.GET app/api/webhooks/trigger/[path]/route.ts:34:10 ❯ app/api/webhooks/trigger/[path]/route.test.ts:414:22
request.method,
undefined,
requestId,
path
) as NextResponse
Comment thread
icecrasher321 marked this conversation as resolved.
Outdated
}

export async function POST(
Expand Down Expand Up @@ -64,6 +70,16 @@
const webhooksForPath = await findAllWebhooksForPath({ requestId, path })

if (webhooksForPath.length === 0) {
const verificationResponse = handlePreLookupWebhookVerification(

Check failure on line 73 in apps/sim/app/api/webhooks/trigger/[path]/route.ts

View workflow job for this annotation

GitHub Actions / Test and Build / Test and Build

app/api/webhooks/trigger/[path]/route.test.ts > Webhook Trigger API Route > should return 200 for empty POST verification probes before webhook persistence

Error: [vitest] No "handlePreLookupWebhookVerification" export is defined on the "@/lib/webhooks/processor" mock. Did you forget to return it from "vi.mock"? If you need to partially mock a module, you can use "importOriginal" helper inside: vi.mock(import("@/lib/webhooks/processor"), async (importOriginal) => { const actual = await importOriginal() return { ...actual, // your mocked methods } }) ❯ Module.POST app/api/webhooks/trigger/[path]/route.ts:73:34 ❯ app/api/webhooks/trigger/[path]/route.test.ts:433:22

Check failure on line 73 in apps/sim/app/api/webhooks/trigger/[path]/route.ts

View workflow job for this annotation

GitHub Actions / Test and Build / Test and Build

app/api/webhooks/trigger/[path]/route.test.ts > Webhook Trigger API Route > should handle 404 for non-existent webhooks

Error: [vitest] No "handlePreLookupWebhookVerification" export is defined on the "@/lib/webhooks/processor" mock. Did you forget to return it from "vi.mock"? If you need to partially mock a module, you can use "importOriginal" helper inside: vi.mock(import("@/lib/webhooks/processor"), async (importOriginal) => { const actual = await importOriginal() return { ...actual, // your mocked methods } }) ❯ Module.POST app/api/webhooks/trigger/[path]/route.ts:73:34 ❯ app/api/webhooks/trigger/[path]/route.test.ts:396:22
request.method,
body,
requestId,
path
)
if (verificationResponse) {
return verificationResponse
}

logger.warn(`[${requestId}] Webhook or workflow not found for path: ${path}`)
return new NextResponse('Not Found', { status: 404 })
}
Expand Down
44 changes: 28 additions & 16 deletions apps/sim/blocks/blocks/grain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const GrainBlock: BlockConfig = {
{ label: 'List Recordings', id: 'grain_list_recordings' },
{ label: 'Get Recording', id: 'grain_get_recording' },
{ label: 'Get Transcript', id: 'grain_get_transcript' },
{ label: 'List Views', id: 'grain_list_views' },
{ label: 'List Teams', id: 'grain_list_teams' },
{ label: 'List Meeting Types', id: 'grain_list_meeting_types' },
{ label: 'Create Webhook', id: 'grain_create_hook' },
Expand Down Expand Up @@ -72,7 +73,7 @@ export const GrainBlock: BlockConfig = {
placeholder: 'ISO8601 timestamp (e.g., 2024-01-01T00:00:00Z)',
condition: {
field: 'operation',
value: ['grain_list_recordings', 'grain_create_hook'],
value: ['grain_list_recordings'],
},
wandConfig: {
enabled: true,
Expand All @@ -96,7 +97,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
placeholder: 'ISO8601 timestamp (e.g., 2024-01-01T00:00:00Z)',
condition: {
field: 'operation',
value: ['grain_list_recordings', 'grain_create_hook'],
value: ['grain_list_recordings'],
},
wandConfig: {
enabled: true,
Expand Down Expand Up @@ -125,7 +126,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
value: () => '',
condition: {
field: 'operation',
value: ['grain_list_recordings', 'grain_create_hook'],
value: ['grain_list_recordings'],
},
},
// Title search
Expand Down Expand Up @@ -162,7 +163,7 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`,
placeholder: 'Filter by team UUID (optional)',
condition: {
field: 'operation',
value: ['grain_list_recordings', 'grain_create_hook'],
value: ['grain_list_recordings'],
},
},
// Meeting type ID filter
Expand All @@ -173,7 +174,7 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`,
placeholder: 'Filter by meeting type UUID (optional)',
condition: {
field: 'operation',
value: ['grain_list_recordings', 'grain_create_hook'],
value: ['grain_list_recordings'],
},
},
// Include highlights
Expand All @@ -183,7 +184,7 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`,
type: 'switch',
condition: {
field: 'operation',
value: ['grain_list_recordings', 'grain_get_recording', 'grain_create_hook'],
value: ['grain_list_recordings', 'grain_get_recording'],
},
},
// Include participants
Expand All @@ -193,7 +194,7 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`,
type: 'switch',
condition: {
field: 'operation',
value: ['grain_list_recordings', 'grain_get_recording', 'grain_create_hook'],
value: ['grain_list_recordings', 'grain_get_recording'],
},
},
// Include AI summary
Expand All @@ -203,7 +204,18 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`,
type: 'switch',
condition: {
field: 'operation',
value: ['grain_list_recordings', 'grain_get_recording', 'grain_create_hook'],
value: ['grain_list_recordings', 'grain_get_recording'],
},
},
{
id: 'viewId',
title: 'View ID',
type: 'short-input',
placeholder: 'Enter Grain view UUID',
required: true,
condition: {
field: 'operation',
value: ['grain_create_hook'],
},
},
// Include calendar event (get_recording only)
Expand Down Expand Up @@ -271,6 +283,7 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`,
'grain_list_recordings',
'grain_get_recording',
'grain_get_transcript',
'grain_list_views',
'grain_list_teams',
'grain_list_meeting_types',
'grain_create_hook',
Expand Down Expand Up @@ -327,24 +340,21 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`,

case 'grain_list_teams':
case 'grain_list_meeting_types':
case 'grain_list_views':
case 'grain_list_hooks':
return baseParams

case 'grain_create_hook':
if (!params.hookUrl?.trim()) {
throw new Error('Webhook URL is required.')
}
if (!params.viewId?.trim()) {
throw new Error('View ID is required.')
}
return {
...baseParams,
hookUrl: params.hookUrl.trim(),
filterBeforeDatetime: params.beforeDatetime || undefined,
filterAfterDatetime: params.afterDatetime || undefined,
filterParticipantScope: params.participantScope || undefined,
filterTeamId: params.teamId || undefined,
filterMeetingTypeId: params.meetingTypeId || undefined,
includeHighlights: params.includeHighlights || false,
includeParticipants: params.includeParticipants || false,
includeAiSummary: params.includeAiSummary || false,
viewId: params.viewId.trim(),
}

case 'grain_delete_hook':
Expand All @@ -367,6 +377,7 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`,
apiKey: { type: 'string', description: 'Grain API key (Personal Access Token)' },
recordingId: { type: 'string', description: 'Recording UUID' },
cursor: { type: 'string', description: 'Pagination cursor' },
viewId: { type: 'string', description: 'Grain view UUID for webhook subscriptions' },
beforeDatetime: {
type: 'string',
description: 'Filter recordings before this ISO8601 timestamp',
Expand Down Expand Up @@ -416,6 +427,7 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`,
teamsList: { type: 'json', description: 'Array of team objects' },
// Meeting type outputs
meetingTypes: { type: 'json', description: 'Array of meeting type objects' },
views: { type: 'json', description: 'Array of Grain views' },
// Hook outputs
hooks: { type: 'json', description: 'Array of webhook objects' },
hook: { type: 'json', description: 'Created webhook data' },
Expand Down
26 changes: 26 additions & 0 deletions apps/sim/lib/webhooks/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,32 @@ export async function handleProviderChallenges(
return null
}

/**
* Returns a verification response for provider reachability probes that happen
* before a webhook row exists and therefore before provider lookup is possible.
*/
export function handlePreLookupWebhookVerification(
method: string,
body: Record<string, unknown> | undefined,
requestId: string,
path: string
): NextResponse | null {
const isVerificationProbe =
method === 'GET' ||
method === 'HEAD' ||
(method === 'POST' && (!body || Object.keys(body).length === 0 || !body.type))
Comment thread
icecrasher321 marked this conversation as resolved.
Outdated

if (!isVerificationProbe) {
return null
}

logger.info(
`[${requestId}] Returning 200 for pre-lookup webhook verification probe on path: ${path}`
)

return NextResponse.json({ status: 'ok', message: 'Webhook endpoint verified' })
}

/**
* Handle provider-specific reachability tests that occur AFTER webhook lookup.
*
Expand Down
Loading
Loading