From 194b58b19687746a6dce7067d3be10d7e40c9568 Mon Sep 17 00:00:00 2001 From: lorentz Date: Wed, 15 Jul 2026 07:48:17 -0400 Subject: [PATCH] feat(15-03): fire-and-forget phishing detection on ticket.created webhook - triggerPhishingDetection() mirrors triggerWorkflowEngine's payload.entity-first shape - reads createdByContactID (Autotask field) into created_by_contact_id, per entity-mapper.ts:211 - called alongside the existing workflow-engine trigger, not awaited in the request path --- lib/services/webhook-service.ts | 43 +++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/lib/services/webhook-service.ts b/lib/services/webhook-service.ts index f2dc4ec..d650ba9 100644 --- a/lib/services/webhook-service.ts +++ b/lib/services/webhook-service.ts @@ -14,6 +14,7 @@ import { workflowEngine } from './workflow-engine'; import { ticketWorkflowEngine } from './ticket-workflow-engine'; import '../services/workflow-steps'; // Register all workflow step executors import { WorkflowEvent, TicketData } from '../types/workflow'; +import { detectPhishingTicket, DetectableTicket } from './phishing-detector'; export class WebhookService { private _autotaskClient: AutotaskClient | null = null; @@ -114,6 +115,9 @@ export class WebhookService { this.triggerWorkflowEngine(payload).catch(err => console.error('[WEBHOOK] Workflow engine error:', err) ); + this.triggerPhishingDetection(payload).catch(err => + console.error('[WEBHOOK] Phishing detection error:', err) + ); } @@ -436,6 +440,45 @@ export class WebhookService { // DEPRECATED: old workflow engine (will be removed after testing period) // await workflowEngine.process(event); } + + /** + * Trigger phishing detection for a new ticket. + * Runs asynchronously — does not block webhook response. Mirrors + * triggerWorkflowEngine's "prefer inline payload.entity, else payload.entityId" shape. + */ + private async triggerPhishingDetection(payload: AutotaskWebhookPayload): Promise { + let ticket: DetectableTicket; + + if (payload.entity) { + // NOTE: Autotask's requester field is `createdByContactID` (mapped to + // created_by_contact_id at lib/utils/entity-mapper.ts:211). There is no + // similarly-named alternative field — since payload.entity is typed + // Record, a wrong field name would compile cleanly but + // silently produce undefined at runtime. + ticket = { + id: payload.entityId, + ticket_number: payload.entity.ticketNumber || null, + title: payload.entity.title || null, + description: payload.entity.description || null, + company_id: payload.entity.companyID || null, + contact_id: payload.entity.contactID || null, + created_by_contact_id: payload.entity.createdByContactID || null, + }; + } else { + ticket = { + id: payload.entityId, + ticket_number: null, + title: null, + description: null, + company_id: null, + contact_id: null, + created_by_contact_id: null, + }; + } + + console.log(`[WEBHOOK] Triggering phishing detection for ticket ${payload.entityId}`); + await detectPhishingTicket(ticket); + } } // Export singleton instance