import { defineLogicFunction, type RoutePayload } from "twenty-sdk/define"; import { Response } from "twenty-sdk/logic-function"; import { WEBHOOK_ROUTE_UNIVERSAL_IDENTIFIER } from "src/constants/universal-identifiers"; import type { SajnWebhookBody, SajnWebhookDocument, } from "src/logic-functions/types/sajn-api.type"; import { jsonResponse } from "src/logic-functions/utils/json-response"; import { getSajnWebhookRegistration } from "src/logic-functions/utils/sajn-webhook-registration"; import { updateSajnDocumentRecord } from "src/logic-functions/utils/update-sajn-document-record"; const safeEqual = (left: string, right: string): boolean => { const leftBytes = new TextEncoder().encode(left); const rightBytes = new TextEncoder().encode(right); const length = Math.max(leftBytes.length, rightBytes.length); let difference = leftBytes.length ^ rightBytes.length; for (let index = 0; index < length; index += 1) { difference |= (leftBytes[index] ?? 0) ^ (rightBytes[index] ?? 0); } return difference === 0; }; const getDocument = ( payload: SajnWebhookBody["payload"], ): SajnWebhookDocument | undefined => { if ("id" in payload) { return payload; } return payload.document; }; const handler = async ( event: RoutePayload, ): Promise => { const registration = await getSajnWebhookRegistration(); const configuredSecret = registration?.secret.trim(); const receivedSecret = event.headers["x-sajn-secret"]?.trim(); if ( !configuredSecret || !receivedSecret || !safeEqual(configuredSecret, receivedSecret) ) { return jsonResponse({ error: "Invalid webhook secret." }, 401); } if (!event.body?.payload) { return jsonResponse({ error: "Webhook payload is missing." }, 400); } const document = getDocument(event.body.payload); if (!document?.id || !document.status) { return jsonResponse( { error: "Webhook does not contain a document." }, 400, ); } const result = await updateSajnDocumentRecord({ sajnDocumentId: document.id, status: document.status, name: document.title, }); return jsonResponse({ received: true, updated: result.found }); }; export default defineLogicFunction({ universalIdentifier: WEBHOOK_ROUTE_UNIVERSAL_IDENTIFIER, name: "sajn-webhook", description: "Synchronizes sajn document lifecycle events into Twenty.", timeoutSeconds: 15, handler, httpRouteTriggerSettings: { path: "/webhook/sajn", httpMethod: "POST", isAuthRequired: false, forwardedRequestHeaders: ["x-sajn-secret", "x-sajn-environment"], }, });