import { createRoute } from "../../modules/api-utils/createRoute"; import { apiResponse } from "../../modules/api-utils/apiResponse"; import { ContentfulStatusCode } from "hono/utils/http-status"; import { z } from "zod"; import GenericError from "../../Errors/GenericError"; type ParsedJson = Record | unknown[]; const callbackResource = z.enum([ "opportunity", "ticket", "company", "activity", ]); const safeParseJson = (value: string): ParsedJson | null => { try { const parsed = JSON.parse(value); const isObject = typeof parsed === "object" && parsed !== null; return isObject ? (parsed as ParsedJson) : null; } catch { return null; } }; const asObject = (value: ParsedJson | null): Record | null => { if (!value) return null; if (Array.isArray(value)) return null; return value; }; const parseJsonStringFields = ( value: Record | null, ): Record | null => { if (!value) return null; return Object.entries(value).reduce>( (acc, [key, current]) => { if (typeof current !== "string") { acc[key] = current; return acc; } const looksLikeJson = current.startsWith("{") || current.startsWith("["); if (!looksLikeJson) { acc[key] = current; return acc; } const parsed = safeParseJson(current); acc[key] = parsed ?? current; return acc; }, {}, ); }; const parseEntity = (value: unknown): ParsedJson | null => { if (typeof value === "string") return safeParseJson(value); if (typeof value !== "object" || value === null) return null; return value as ParsedJson; }; const buildSummary = ( resource: z.infer, parsedBody: Record | null, parsedEntity: Record | null, ) => { if (!parsedBody) return null; return { resource, messageId: parsedBody.MessageId ?? null, action: parsedBody.Action ?? null, type: parsedBody.Type ?? null, id: parsedBody.ID ?? null, memberId: parsedBody.MemberId ?? null, entityStatus: parsedEntity?.StatusName ?? parsedEntity?.TicketStatus ?? parsedEntity?.Status ?? null, entitySummary: parsedEntity?.Summary ?? parsedEntity?.CompanyName ?? null, entityUpdatedBy: parsedEntity?.UpdatedBy ?? null, entityLastUpdated: parsedEntity?.LastUpdatedUTC ?? parsedEntity?.LastUpdated ?? null, }; }; const parseHeaders = (headers: Headers): Record => Object.fromEntries(headers.entries()); const callbackHeaderSummary = (headers: Record) => ({ contentType: headers["content-type"] ?? null, userAgent: headers["user-agent"] ?? null, host: headers.host ?? null, forwardedFor: headers["x-forwarded-for"] ?? null, callbackId: headers["x-cw-request-id"] ?? headers["x-request-id"] ?? headers["x-correlation-id"] ?? null, }); /* /v1/cw/callback/:resource */ export default createRoute("post", ["/callback/:secret/:resource"], async (c) => { const suppliedSecret = c.req.param("secret"); const expectedSecret = process.env.CW_CALLBACK_SECRET; if (expectedSecret && suppliedSecret !== expectedSecret) { throw new GenericError({ name: "Unauthorized", message: "Invalid callback secret.", cause: "Path secret mismatch", status: 401, }); } if (!expectedSecret) { console.warn( "[cw-callback] CW_CALLBACK_SECRET is not configured; accepting path secret without verification", ); } const resource = callbackResource.parse(c.req.param("resource")); const headers = parseHeaders(c.req.raw.headers); const headerSummary = callbackHeaderSummary(headers); const rawBody = await c.req.text(); const parsedJson = safeParseJson(rawBody); const parsedBody = asObject(parsedJson); const parsedBodyExpanded = parseJsonStringFields(parsedBody); const parsedEntity = asObject(parseEntity(parsedBodyExpanded?.Entity)); const summary = buildSummary(resource, parsedBodyExpanded, parsedEntity); const line = [ `[cw-callback] resource=${resource}`, `action=${String(summary?.action ?? "-")}`, `type=${String(summary?.type ?? "-")}`, `id=${String(summary?.id ?? "-")}`, `by=${String(summary?.entityUpdatedBy ?? summary?.memberId ?? "-")}`, `requestId=${String(headerSummary.callbackId ?? "-")}`, `status=${String(summary?.entityStatus ?? "-")}`, `summary=${String(summary?.entitySummary ?? "-")}`, ].join(" "); console.log(line); const response = apiResponse.successful("CW callback received.", { resource, secretValidated: Boolean(expectedSecret), summary, headers, headerSummary, bodyParsed: parsedBodyExpanded, receivedAt: new Date().toISOString(), }); return c.json(response, response.status as ContentfulStatusCode); });