import { createLogger } from '@sim/logger' import { NextResponse } from 'next/server' import type { AuthContext, EventMatchContext, FormatInputContext, FormatInputResult, WebhookProviderHandler, } from '@/lib/webhooks/providers/types' import { buildFallbackDeliveryFingerprint, verifyTokenAuth } from '@/lib/webhooks/providers/utils' export function extractSalesforceObjectTypeFromPayload( body: Record ): string | undefined { const direct = (typeof body.objectType === 'string' && body.objectType) || (typeof body.sobjectType === 'string' && body.sobjectType) || undefined if (direct) { return direct } const attrs = body.attributes as Record | undefined if (typeof attrs?.type === 'string') { return attrs.type } const record = body.record if (record && typeof record === 'object' && !Array.isArray(record)) { const r = record as Record if (typeof r.sobjectType === 'string') { return r.sobjectType } const ra = r.attributes as Record | undefined if (typeof ra?.type === 'string') { return ra.type } } return undefined } const logger = createLogger('WebhookProvider:Salesforce') function verifySalesforceSharedSecret(request: Request, secret: string): boolean { if (verifyTokenAuth(request, secret, 'x-sim-webhook-secret')) { return true } return verifyTokenAuth(request, secret) } function asRecord(body: unknown): Record { return body && typeof body === 'object' && !Array.isArray(body) ? (body as Record) : {} } function extractRecordCore(body: Record): Record { const nested = body.record if (nested && typeof nested === 'object' && !Array.isArray(nested)) { return { ...(nested as Record) } } const skip = new Set([ 'eventType', 'simEventType', 'changedFields', 'previousStage', 'newStage', 'previousStatus', 'newStatus', 'payload', 'record', ]) const out: Record = {} for (const [k, v] of Object.entries(body)) { if (!skip.has(k)) { out[k] = v } } return out } function pickTimestamp(body: Record, record: Record): string { const candidates = [ body.timestamp, body.time, record.SystemModstamp, record.LastModifiedDate, record.CreatedDate, ] for (const c of candidates) { if (typeof c === 'string' && c.length > 0) { return c } } return '' } function pickRecordId(body: Record, record: Record): string { const id = (typeof body.recordId === 'string' && body.recordId) || (typeof record.Id === 'string' && record.Id) || (typeof body.Id === 'string' && body.Id) || '' return id } function pickStr(record: Record, key: string): string { const v = record[key] return typeof v === 'string' ? v : '' } export const salesforceHandler: WebhookProviderHandler = { verifyAuth({ request, requestId, providerConfig }: AuthContext): NextResponse | null { const secret = providerConfig.webhookSecret as string | undefined if (!secret?.trim()) { logger.warn(`[${requestId}] Salesforce webhook missing webhookSecret — rejecting`) return new NextResponse('Unauthorized - Webhook secret not configured', { status: 401 }) } if (!verifySalesforceSharedSecret(request, secret.trim())) { logger.warn(`[${requestId}] Salesforce webhook secret verification failed`) return new NextResponse('Unauthorized - Invalid webhook secret', { status: 401 }) } return null }, async matchEvent({ webhook, workflow, body, requestId, providerConfig }: EventMatchContext) { const triggerId = providerConfig.triggerId as string | undefined if (!triggerId) { return true } const { isSalesforceEventMatch } = await import('@/triggers/salesforce/utils') const configuredObjectType = providerConfig.objectType as string | undefined const obj = asRecord(body) if (!isSalesforceEventMatch(triggerId, obj, configuredObjectType)) { logger.debug( `[${requestId}] Salesforce event mismatch for trigger ${triggerId}. Skipping execution.`, { webhookId: webhook.id, workflowId: workflow.id, triggerId } ) return false } return true }, async formatInput(ctx: FormatInputContext): Promise { const rawPc = (ctx.webhook as { providerConfig?: unknown }).providerConfig const pc = rawPc && typeof rawPc === 'object' && !Array.isArray(rawPc) ? (rawPc as Record) : {} const id = typeof pc.triggerId === 'string' ? pc.triggerId : '' const body = asRecord(ctx.body) const record = extractRecordCore(body) const objectType = extractSalesforceObjectTypeFromPayload(body) || (typeof record.attributes === 'object' && record.attributes && typeof (record.attributes as Record).type === 'string' ? String((record.attributes as Record).type) : '') || (typeof record.sobjectType === 'string' ? record.sobjectType : '') const recordId = pickRecordId(body, record) const timestamp = pickTimestamp(body, record) const eventTypeRaw = (typeof body.eventType === 'string' && body.eventType) || (typeof body.simEventType === 'string' && body.simEventType) || '' const simEventTypeRaw = typeof body.simEventType === 'string' ? body.simEventType : '' if (id === 'salesforce_webhook') { return { input: { eventType: eventTypeRaw || 'webhook', objectType: objectType || '', recordId, timestamp, simEventType: simEventTypeRaw, record: Object.keys(record).length > 0 ? record : body, payload: ctx.body, }, } } if ( id === 'salesforce_record_created' || id === 'salesforce_record_updated' || id === 'salesforce_record_deleted' ) { const changedFields = body.changedFields return { input: { eventType: eventTypeRaw || id.replace('salesforce_', '').replace(/_/g, ' '), objectType: objectType || '', recordId, timestamp, simEventType: simEventTypeRaw, record: { Id: typeof record.Id === 'string' ? record.Id : recordId, Name: typeof record.Name === 'string' ? record.Name : '', CreatedDate: typeof record.CreatedDate === 'string' ? record.CreatedDate : '', LastModifiedDate: typeof record.LastModifiedDate === 'string' ? record.LastModifiedDate : '', OwnerId: pickStr(record, 'OwnerId'), SystemModstamp: pickStr(record, 'SystemModstamp'), }, changedFields: changedFields !== undefined ? changedFields : null, payload: ctx.body, }, } } if (id === 'salesforce_opportunity_stage_changed') { return { input: { eventType: eventTypeRaw || 'opportunity_stage_changed', objectType: objectType || 'Opportunity', recordId, timestamp, simEventType: simEventTypeRaw, record: { Id: typeof record.Id === 'string' ? record.Id : recordId, Name: typeof record.Name === 'string' ? record.Name : '', StageName: typeof record.StageName === 'string' ? record.StageName : '', Amount: record.Amount !== undefined ? String(record.Amount) : '', CloseDate: typeof record.CloseDate === 'string' ? record.CloseDate : '', Probability: record.Probability !== undefined ? String(record.Probability) : '', AccountId: pickStr(record, 'AccountId'), OwnerId: pickStr(record, 'OwnerId'), }, previousStage: typeof body.previousStage === 'string' ? body.previousStage : typeof body.PriorStage === 'string' ? body.PriorStage : '', newStage: typeof body.newStage === 'string' ? body.newStage : typeof record.StageName === 'string' ? record.StageName : '', payload: ctx.body, }, } } if (id === 'salesforce_case_status_changed') { return { input: { eventType: eventTypeRaw || 'case_status_changed', objectType: objectType || 'Case', recordId, timestamp, simEventType: simEventTypeRaw, record: { Id: typeof record.Id === 'string' ? record.Id : recordId, Subject: typeof record.Subject === 'string' ? record.Subject : '', Status: typeof record.Status === 'string' ? record.Status : '', Priority: typeof record.Priority === 'string' ? record.Priority : '', CaseNumber: typeof record.CaseNumber === 'string' ? record.CaseNumber : '', AccountId: pickStr(record, 'AccountId'), ContactId: pickStr(record, 'ContactId'), OwnerId: pickStr(record, 'OwnerId'), }, previousStatus: typeof body.previousStatus === 'string' ? body.previousStatus : typeof body.PriorStatus === 'string' ? body.PriorStatus : '', newStatus: typeof body.newStatus === 'string' ? body.newStatus : typeof record.Status === 'string' ? record.Status : '', payload: ctx.body, }, } } return { input: { eventType: eventTypeRaw || 'webhook', objectType: objectType || '', recordId, timestamp, simEventType: simEventTypeRaw, record: Object.keys(record).length > 0 ? record : body, payload: ctx.body, }, } }, extractIdempotencyId(body: unknown): string | null { const b = asRecord(body) const record = extractRecordCore(b) const id = pickRecordId(b, record) const et = (typeof b.eventType === 'string' && b.eventType) || (typeof b.simEventType === 'string' && b.simEventType) || '' const ts = pickTimestamp(b, record) if (!id) { return null } if (ts) { return `salesforce:${et || 'event'}:${id}:${ts}` } return `salesforce:${et || 'event'}:${id}:${buildFallbackDeliveryFingerprint(b)}` }, }