chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,320 @@
|
||||
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, unknown>
|
||||
): 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<string, unknown> | 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<string, unknown>
|
||||
if (typeof r.sobjectType === 'string') {
|
||||
return r.sobjectType
|
||||
}
|
||||
const ra = r.attributes as Record<string, unknown> | 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<string, unknown> {
|
||||
return body && typeof body === 'object' && !Array.isArray(body)
|
||||
? (body as Record<string, unknown>)
|
||||
: {}
|
||||
}
|
||||
|
||||
function extractRecordCore(body: Record<string, unknown>): Record<string, unknown> {
|
||||
const nested = body.record
|
||||
if (nested && typeof nested === 'object' && !Array.isArray(nested)) {
|
||||
return { ...(nested as Record<string, unknown>) }
|
||||
}
|
||||
|
||||
const skip = new Set([
|
||||
'eventType',
|
||||
'simEventType',
|
||||
'changedFields',
|
||||
'previousStage',
|
||||
'newStage',
|
||||
'previousStatus',
|
||||
'newStatus',
|
||||
'payload',
|
||||
'record',
|
||||
])
|
||||
const out: Record<string, unknown> = {}
|
||||
for (const [k, v] of Object.entries(body)) {
|
||||
if (!skip.has(k)) {
|
||||
out[k] = v
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function pickTimestamp(body: Record<string, unknown>, record: Record<string, unknown>): 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<string, unknown>, record: Record<string, unknown>): 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<string, unknown>, 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<FormatInputResult> {
|
||||
const rawPc = (ctx.webhook as { providerConfig?: unknown }).providerConfig
|
||||
const pc =
|
||||
rawPc && typeof rawPc === 'object' && !Array.isArray(rawPc)
|
||||
? (rawPc as Record<string, unknown>)
|
||||
: {}
|
||||
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<string, unknown>).type === 'string'
|
||||
? String((record.attributes as Record<string, unknown>).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)}`
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user