import { createLogger } from '@sim/logger' import { safeCompare } from '@sim/security/compare' import { hmacSha256Hex } from '@sim/security/hmac' import { isRecordLike } from '@sim/utils/object' import type { EventMatchContext, FormatInputContext, FormatInputResult, WebhookProviderHandler, } from '@/lib/webhooks/providers/types' import { createHmacVerifier } from '@/lib/webhooks/providers/utils' const logger = createLogger('WebhookProvider:Jira') /** * `changelog.items[].field` lists the Jira field names that changed in this * update (only present on issue_updated deliveries). Empty filter list means * no restriction — match on any field change. */ function matchesFieldFilters(body: Record, fieldFilters: string): boolean { const wanted = fieldFilters .split(',') .map((f) => f.trim().toLowerCase()) .filter(Boolean) if (wanted.length === 0) return true const changelog = body.changelog as Record | undefined const items = Array.isArray(changelog?.items) ? changelog.items : [] const changedFields = items .map((item) => (isRecordLike(item) && typeof item.field === 'string' ? item.field : '')) .map((f) => f.toLowerCase()) return wanted.some((field) => changedFields.includes(field)) } export function validateJiraSignature(secret: string, signature: string, body: string): boolean { try { if (!secret || !signature || !body) { logger.warn('Jira signature validation missing required fields', { hasSecret: !!secret, hasSignature: !!signature, hasBody: !!body, }) return false } if (!signature.startsWith('sha256=')) { logger.warn('Jira signature has invalid format (expected sha256=)', { signaturePrefix: signature.substring(0, 10), }) return false } const providedSignature = signature.substring(7) const computedHash = hmacSha256Hex(body, secret) logger.debug('Jira signature comparison', { computedLength: computedHash.length, providedLength: providedSignature.length, }) return safeCompare(computedHash, providedSignature) } catch (error) { logger.error('Error validating Jira signature:', error) return false } } export const jiraHandler: WebhookProviderHandler = { verifyAuth: createHmacVerifier({ configKey: 'webhookSecret', headerName: 'X-Hub-Signature', validateFn: validateJiraSignature, providerLabel: 'Jira', }), async formatInput({ body, webhook }: FormatInputContext): Promise { const { extractIssueData, extractCommentData, extractWorklogData, extractSprintData, extractProjectData, extractVersionData, } = await import('@/triggers/jira/utils') const providerConfig = (webhook.providerConfig as Record) || {} const triggerId = providerConfig.triggerId as string | undefined if ( triggerId === 'jira_issue_commented' || triggerId === 'jira_comment_updated' || triggerId === 'jira_comment_deleted' ) { return { input: extractCommentData(body) } } if ( triggerId === 'jira_worklog_created' || triggerId === 'jira_worklog_updated' || triggerId === 'jira_worklog_deleted' ) { return { input: extractWorklogData(body) } } if ( triggerId === 'jira_sprint_created' || triggerId === 'jira_sprint_started' || triggerId === 'jira_sprint_closed' ) { return { input: extractSprintData(body) } } if (triggerId === 'jira_project_created') { return { input: extractProjectData(body) } } if (triggerId === 'jira_version_released') { return { input: extractVersionData(body) } } if (!triggerId || triggerId === 'jira_webhook') { const obj = isRecordLike(body) ? body : {} return { input: { webhookEvent: obj.webhookEvent, timestamp: obj.timestamp, user: obj.user || null, issue_event_type_name: obj.issue_event_type_name, issue: obj.issue || {}, changelog: obj.changelog, comment: obj.comment, worklog: obj.worklog, sprint: obj.sprint, project: obj.project, version: obj.version, }, } } return { input: extractIssueData(body) } }, async matchEvent({ webhook, workflow, body, requestId, providerConfig }: EventMatchContext) { const triggerId = providerConfig.triggerId as string | undefined const obj = isRecordLike(body) ? body : {} if (triggerId && triggerId !== 'jira_webhook') { const webhookEvent = obj.webhookEvent as string | undefined const issueEventTypeName = obj.issue_event_type_name as string | undefined const { isJiraEventMatch } = await import('@/triggers/jira/utils') if (!isJiraEventMatch(triggerId, webhookEvent || '', issueEventTypeName)) { logger.debug( `[${requestId}] Jira event mismatch for trigger ${triggerId}. Event: ${webhookEvent}. Skipping execution.`, { webhookId: webhook.id, workflowId: workflow.id, triggerId, receivedEvent: webhookEvent, } ) return false } const fieldFilters = providerConfig.fieldFilters as string | undefined if ( triggerId === 'jira_issue_updated' && fieldFilters && !matchesFieldFilters(obj, fieldFilters) ) { logger.debug( `[${requestId}] Jira issue_updated field filter did not match for trigger ${triggerId}. Skipping execution.`, { webhookId: webhook.id, workflowId: workflow.id, fieldFilters } ) return false } } return true }, extractIdempotencyId(body: unknown) { const obj = isRecordLike(body) ? body : {} const issue = obj.issue as Record | undefined const comment = obj.comment as Record | undefined const worklog = obj.worklog as Record | undefined const project = obj.project as Record | undefined const sprint = obj.sprint as Record | undefined const version = obj.version as Record | undefined const entityId = comment?.id || worklog?.id || issue?.id || project?.id || sprint?.id || version?.id if (obj.webhookEvent && entityId) { const ts = obj.timestamp ?? '' return `${obj.webhookEvent}:${entityId}:${ts}` } return null }, }