import { createLogger } from '@sim/logger' import { NextResponse } from 'next/server' import { getClientIp } from '@/lib/core/utils/request' import type { AuthContext, EventFilterContext, FormatInputContext, FormatInputResult, ProcessFilesContext, WebhookProviderHandler, } from '@/lib/webhooks/providers/types' import { verifyTokenAuth } from '@/lib/webhooks/providers/utils' const logger = createLogger('WebhookProvider:Generic') export const genericHandler: WebhookProviderHandler = { verifyAuth({ request, requestId, providerConfig }: AuthContext) { if (providerConfig.requireAuth) { const configToken = providerConfig.token as string | undefined if (!configToken) { return new NextResponse('Unauthorized - Authentication required but no token configured', { status: 401, }) } const secretHeaderName = providerConfig.secretHeaderName as string | undefined if (!verifyTokenAuth(request, configToken, secretHeaderName)) { return new NextResponse('Unauthorized - Invalid authentication token', { status: 401 }) } } const allowedIps = providerConfig.allowedIps if (allowedIps && Array.isArray(allowedIps) && allowedIps.length > 0) { const clientIp = getClientIp(request) if (clientIp === 'unknown' || !allowedIps.includes(clientIp)) { logger.warn(`[${requestId}] Forbidden webhook access attempt - IP not allowed: ${clientIp}`) return new NextResponse('Forbidden - IP not allowed', { status: 403, }) } } return null }, enrichHeaders({ body, providerConfig }: EventFilterContext, headers: Record) { const idempotencyField = providerConfig.idempotencyField as string | undefined if (idempotencyField && body) { const value = idempotencyField .split('.') .reduce( (acc: unknown, key: string) => acc && typeof acc === 'object' ? (acc as Record)[key] : undefined, body ) if (value !== undefined && value !== null && typeof value !== 'object') { headers['x-sim-idempotency-key'] = String(value) } } }, formatSuccessResponse(providerConfig: Record) { if (providerConfig.responseMode === 'custom') { const rawCode = Number(providerConfig.responseStatusCode) || 200 const statusCode = rawCode >= 100 && rawCode <= 599 ? rawCode : 200 const responseBody = (providerConfig.responseBody as string | undefined)?.trim() if (!responseBody) { return new NextResponse(null, { status: statusCode }) } try { const parsed = JSON.parse(responseBody) return NextResponse.json(parsed, { status: statusCode }) } catch { return new NextResponse(responseBody, { status: statusCode, headers: { 'Content-Type': 'text/plain' }, }) } } return null }, async formatInput({ body }: FormatInputContext): Promise { return { input: body } }, async processInputFiles({ input, blocks, blockId, workspaceId, workflowId, executionId, requestId, userId, }: ProcessFilesContext) { const triggerBlock = blocks[blockId] as Record | undefined const subBlocks = triggerBlock?.subBlocks as Record | undefined const inputFormatBlock = subBlocks?.inputFormat as Record | undefined if (inputFormatBlock?.value) { const inputFormat = inputFormatBlock.value as Array<{ name: string type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'file[]' }> const fileFields = inputFormat.filter((field) => field.type === 'file[]') if (fileFields.length > 0) { const { processExecutionFiles } = await import('@/lib/execution/files') const executionContext = { workspaceId, workflowId, executionId, } for (const fileField of fileFields) { const fieldValue = input[fileField.name] if (fieldValue && typeof fieldValue === 'object') { const uploadedFiles = await processExecutionFiles( fieldValue, executionContext, requestId, userId ) if (uploadedFiles.length > 0) { input[fileField.name] = uploadedFiles logger.info( `[${requestId}] Successfully processed ${uploadedFiles.length} file(s) for field: ${fileField.name}` ) } } } } } }, }