@adatechnology/meta-whatsapp-contracts 0.2.0-rc.5 → 0.2.0-rc.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +8 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +63 -2
- package/dist/index.d.ts +63 -2
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/testing/index.cjs +27 -0
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +18 -1
- package/dist/testing/index.d.ts +18 -1
- package/dist/testing/index.js +26 -0
- package/dist/testing/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -24,6 +24,7 @@ __export(index_exports, {
|
|
|
24
24
|
CROSS_FLOW_PREFIX: () => CROSS_FLOW_PREFIX,
|
|
25
25
|
ConfigMissingError: () => ConfigMissingError,
|
|
26
26
|
DuplicateWebhookDeliveryError: () => DuplicateWebhookDeliveryError,
|
|
27
|
+
FLOW_ACTION_KIND: () => FLOW_ACTION_KIND,
|
|
27
28
|
InvalidWebhookSignatureError: () => InvalidWebhookSignatureError,
|
|
28
29
|
META_WHATSAPP_ERROR_CODES: () => META_WHATSAPP_ERROR_CODES,
|
|
29
30
|
MetaWhatsAppError: () => MetaWhatsAppError,
|
|
@@ -141,6 +142,12 @@ var whatsAppWebhookPayloadSchema = import_zod.z.object({
|
|
|
141
142
|
|
|
142
143
|
// src/flow.types.ts
|
|
143
144
|
var import_zod2 = require("zod");
|
|
145
|
+
var FLOW_ACTION_KIND = {
|
|
146
|
+
HANDOFF: "handoff",
|
|
147
|
+
RATE_LIMITED_HANDOFF: "rate_limited_handoff",
|
|
148
|
+
SEND_PRODUCT_LIST: "send_product_list",
|
|
149
|
+
SEND_MEDIA: "send_media"
|
|
150
|
+
};
|
|
144
151
|
var CROSS_FLOW_PREFIX = "flow:";
|
|
145
152
|
var isCrossFlowTarget = /* @__PURE__ */ __name((target) => target.startsWith(CROSS_FLOW_PREFIX), "isCrossFlowTarget");
|
|
146
153
|
var crossFlowKey = /* @__PURE__ */ __name((target) => target.slice(CROSS_FLOW_PREFIX.length), "crossFlowKey");
|
|
@@ -280,6 +287,7 @@ var SessionNotFoundError = class extends MetaWhatsAppError {
|
|
|
280
287
|
CROSS_FLOW_PREFIX,
|
|
281
288
|
ConfigMissingError,
|
|
282
289
|
DuplicateWebhookDeliveryError,
|
|
290
|
+
FLOW_ACTION_KIND,
|
|
283
291
|
InvalidWebhookSignatureError,
|
|
284
292
|
META_WHATSAPP_ERROR_CODES,
|
|
285
293
|
MetaWhatsAppError,
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/webhook.types.ts","../src/flow.types.ts","../src/errors.ts"],"sourcesContent":["export {\n whatsAppMediaSchema,\n whatsAppInteractiveSchema,\n whatsAppOrderSchema,\n whatsAppMessageSchema,\n whatsAppMessageEchoSchema,\n whatsAppMessageStatusSchema,\n whatsAppStatusSchema,\n whatsAppWebhookPayloadSchema,\n} from './webhook.types'\nexport type {\n WhatsAppMedia,\n WhatsAppInteractive,\n WhatsAppOrder,\n WhatsAppMessage,\n WhatsAppMessageEcho,\n WhatsAppMessageStatusValue,\n WhatsAppStatus,\n WhatsAppWebhookPayload,\n} from './webhook.types'\n\nexport type {\n SessionState,\n SessionMode,\n MessageDirection,\n MessageSender,\n MessageStatus,\n ConversationSession,\n ConversationMessage,\n ConversationSummary,\n ListConversationsParams,\n ListMessagesParams,\n} from './conversation.types'\n\nexport {\n CROSS_FLOW_PREFIX,\n isCrossFlowTarget,\n crossFlowKey,\n flowNodeNextSchema,\n flowNodeDataSchema,\n flowGraphNodesSchema,\n} from './flow.types'\nexport type {\n FlowNodeType,\n FlowQuestionType,\n FlowActionKind,\n FlowConditionOperator,\n FlowNodeNext,\n FlowNodeData,\n FlowGraphData,\n FlowGraphSummary,\n LiveFlowPosition,\n} from './flow.types'\n\nexport type {\n WhatsAppSettings,\n TemplateVariablesMap,\n TemplateConfig,\n WhatsAppTemplateSummary,\n CreateTemplateResult,\n} from './settings.types'\n\nexport type { MetaWhatsAppHooks, MessageHookOutcome } from './events'\n\nexport type {\n ChannelAdapterInterface,\n SubjectResolverInterface,\n CatalogProduct,\n CatalogPort,\n ObjectStorageInterface,\n RealtimeNotifierInterface,\n FlowActionHandler,\n FlowActionResult,\n FlowActionRegistry,\n} from './providers'\n\nexport {\n MetaWhatsAppError,\n META_WHATSAPP_ERROR_CODES,\n WindowExpiredError,\n InvalidWebhookSignatureError,\n DuplicateWebhookDeliveryError,\n ConfigMissingError,\n TemplateNotConfiguredError,\n SessionNotFoundError,\n} from './errors'\n","import { z } from 'zod'\n\n// Extraído de financiamento-imobiliario-bot/apps/api/src/modules/webhook/application/use-cases/\n// ReceiveWhatsAppWebhook.use-case.ts:39-113 — shape real do payload da Cloud API da Meta.\n\nexport const whatsAppMediaSchema = z.object({\n id: z.string(),\n mime_type: z.string(),\n sha256: z.string().optional(),\n caption: z.string().optional(),\n filename: z.string().optional(),\n})\nexport type WhatsAppMedia = z.infer<typeof whatsAppMediaSchema>\n\nexport const whatsAppInteractiveSchema = z.object({\n type: z.string(),\n button_reply: z.object({ id: z.string(), title: z.string() }).optional(),\n list_reply: z.object({ id: z.string(), title: z.string() }).optional(),\n})\nexport type WhatsAppInteractive = z.infer<typeof whatsAppInteractiveSchema>\n\nexport const whatsAppOrderSchema = z.object({\n catalog_id: z.string(),\n text: z.string().optional(),\n product_items: z.array(\n z.object({\n product_retailer_id: z.string(),\n quantity: z.number(),\n item_price: z.number(),\n currency: z.string(),\n }),\n ),\n})\nexport type WhatsAppOrder = z.infer<typeof whatsAppOrderSchema>\n\nexport const whatsAppMessageSchema = z.object({\n id: z.string(),\n from: z.string(),\n type: z.string(),\n text: z.object({ body: z.string() }).optional(),\n image: whatsAppMediaSchema.optional(),\n audio: whatsAppMediaSchema.optional(),\n video: whatsAppMediaSchema.optional(),\n document: whatsAppMediaSchema.optional(),\n sticker: whatsAppMediaSchema.optional(),\n interactive: whatsAppInteractiveSchema.optional(),\n // Pedido enviado pelo carrinho do catálogo (WhatsApp Commerce)\n order: whatsAppOrderSchema.optional(),\n // Presente quando o cliente abre um item do catálogo e manda mensagem pela página do produto\n context: z\n .object({\n from: z.string().optional(),\n id: z.string().optional(),\n referred_product: z.object({ catalog_id: z.string(), product_retailer_id: z.string() }).optional(),\n })\n .optional(),\n timestamp: z.string(),\n})\nexport type WhatsAppMessage = z.infer<typeof whatsAppMessageSchema>\n\nexport const whatsAppMessageEchoSchema = z.object({\n id: z.string(),\n from: z.string(),\n timestamp: z.string(),\n type: z.string(),\n})\nexport type WhatsAppMessageEcho = z.infer<typeof whatsAppMessageEchoSchema>\n\nexport const whatsAppMessageStatusSchema = z.enum(['sent', 'delivered', 'read', 'failed'])\nexport type WhatsAppMessageStatusValue = z.infer<typeof whatsAppMessageStatusSchema>\n\nexport const whatsAppStatusSchema = z.object({\n id: z.string(),\n status: whatsAppMessageStatusSchema,\n timestamp: z.string(),\n recipient_id: z.string().optional(),\n})\nexport type WhatsAppStatus = z.infer<typeof whatsAppStatusSchema>\n\nexport const whatsAppWebhookPayloadSchema = z.object({\n object: z.string(),\n entry: z.array(\n z.object({\n id: z.string(),\n changes: z.array(\n z.object({\n value: z.object({\n messaging_product: z.string(),\n messages: z.array(whatsAppMessageSchema).optional(),\n message_echoes: z.array(whatsAppMessageEchoSchema).optional(),\n statuses: z.array(whatsAppStatusSchema).optional(),\n metadata: z.object({ display_phone_number: z.string(), phone_number_id: z.string() }).optional(),\n }),\n }),\n ),\n }),\n ),\n})\nexport type WhatsAppWebhookPayload = z.infer<typeof whatsAppWebhookPayloadSchema>\n","// Mesmo shape de dados do editor visual (@adatechnology/conversations-ui/flows) — o backend\n// interpreta/persiste o grafo, o frontend edita; os dois lados precisam concordar no formato.\n// `FlowActionKind` é string aberta (não union fechada): o host registra os próprios actions\n// (ex.: 'trigger_simulation' no bot) via registerFlowAction() no módulo (T4.3) — o pacote nunca\n// assume nenhum caso de negócio específico.\nimport { z } from 'zod'\n\nexport type FlowNodeType = 'question' | 'entrada_choice' | 'action' | 'menu' | 'condition'\nexport type FlowQuestionType = 'text' | 'money' | 'date' | 'int' | 'cpf' | 'choice'\nexport type FlowActionKind = string\nexport type FlowConditionOperator = '>' | '>=' | '<' | '<=' | '==' | '!=' | 'contains'\nexport type FlowNodeNext = string | { byAnswer: Record<string, string>; default: string }\n\n// Destinos \"flow:<key>\" são saltos para outro fluxo — mesma convenção usada pelo editor visual\n// (conversations-ui/flows/flowGraph.ts); backend e frontend precisam concordar no formato.\nexport const CROSS_FLOW_PREFIX = 'flow:'\nexport const isCrossFlowTarget = (target: string): boolean => target.startsWith(CROSS_FLOW_PREFIX)\nexport const crossFlowKey = (target: string): string => target.slice(CROSS_FLOW_PREFIX.length)\n\nexport type FlowNodeData = {\n id: string\n type: FlowNodeType\n contextKey?: string\n questionType?: FlowQuestionType\n question?: string\n options?: [string, string][]\n actionKind?: FlowActionKind\n // Parâmetros arbitrários que o nó carrega para o handler registrado em `actionKind`. O pacote\n // nunca os interpreta — é o que permite um nó de ação ser configurado pelo editor sem que o\n // vocabulário do produto (o antigo `simulationTemplate`) vaze para dentro do contrato.\n actionParams?: Record<string, unknown>\n directMessage?: string\n fallbackMessage?: string\n conditionContextKey?: string\n conditionOperator?: FlowConditionOperator\n conditionValue?: string\n position?: { x: number; y: number }\n next?: FlowNodeNext\n}\n\nexport interface FlowGraphData {\n key: string\n label: string\n startNodeId: string\n version: number\n nodes: Record<string, FlowNodeData>\n}\n\nexport interface FlowGraphSummary {\n key: string\n label: string\n nodeCount: number\n showInMenu: boolean\n menuOptionLabel?: string\n updatedAt: string\n}\n\n// Retorno de GetLiveFlowPositions (T4.2) — quantas sessões ativas estão em cada nó agora,\n// usado pelo editor para desenhar o \"liveCount\" nos cards (FlowNodeCardData.liveCount).\nexport interface LiveFlowPosition {\n flowKey: string\n nodeId: string\n count: number\n}\n\n// Validação de runtime do grafo. O `nodes` é um jsonb que entrou pelo editor (dado de origem\n// cliente) e sai do banco como `unknown` — sem parse, um grafo malformado só se manifestaria\n// como comportamento estranho lá dentro do interpretador, longe da causa.\n//\n// Permissivo de propósito em dois pontos: `actionKind` é string aberta (o host registra os\n// seus) e `.passthrough()` deixa passar campos extras — um grafo salvo por uma versão mais nova\n// do editor não pode ficar irrecuperável numa versão mais antiga do módulo.\nexport const flowNodeNextSchema = z.union([\n z.string(),\n z.object({ byAnswer: z.record(z.string(), z.string()), default: z.string() }),\n])\n\nexport const flowNodeDataSchema = z\n .object({\n id: z.string(),\n type: z.enum(['question', 'entrada_choice', 'action', 'menu', 'condition']),\n contextKey: z.string().optional(),\n questionType: z.enum(['text', 'money', 'date', 'int', 'cpf', 'choice']).optional(),\n question: z.string().optional(),\n options: z.array(z.tuple([z.string(), z.string()])).optional(),\n actionKind: z.string().optional(),\n actionParams: z.record(z.string(), z.unknown()).optional(),\n directMessage: z.string().optional(),\n fallbackMessage: z.string().optional(),\n conditionContextKey: z.string().optional(),\n conditionOperator: z.enum(['>', '>=', '<', '<=', '==', '!=', 'contains']).optional(),\n conditionValue: z.string().optional(),\n position: z.object({ x: z.number(), y: z.number() }).optional(),\n next: flowNodeNextSchema.optional(),\n })\n .passthrough()\n\nexport const flowGraphNodesSchema = z.record(z.string(), flowNodeDataSchema)\n","// Hierarquia de erros do módulo — não depende do framework de erros do host (nenhum DomainError\n// importado); o host mapeia estes para o seu próprio formato de resposta no exception filter.\nexport class MetaWhatsAppError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n public readonly code: string,\n public readonly details?: Record<string, unknown>,\n ) {\n super(message)\n this.name = 'MetaWhatsAppError'\n }\n}\n\nexport const META_WHATSAPP_ERROR_CODES = {\n WINDOW_EXPIRED: 'META_WHATSAPP_WINDOW_EXPIRED',\n INVALID_SIGNATURE: 'META_WHATSAPP_INVALID_SIGNATURE',\n DUPLICATE_DELIVERY: 'META_WHATSAPP_DUPLICATE_DELIVERY',\n CONFIG_MISSING: 'META_WHATSAPP_CONFIG_MISSING',\n TEMPLATE_NOT_CONFIGURED: 'META_WHATSAPP_TEMPLATE_NOT_CONFIGURED',\n SESSION_NOT_FOUND: 'META_WHATSAPP_SESSION_NOT_FOUND',\n} as const\n\nexport class WindowExpiredError extends MetaWhatsAppError {\n constructor(public readonly hoursSinceLastMessage?: number) {\n super(\n 'Janela de 24h expirada — não é possível enviar mensagem para este contato fora de template.',\n 400,\n META_WHATSAPP_ERROR_CODES.WINDOW_EXPIRED,\n { hoursSinceLastMessage },\n )\n }\n}\n\n// Assinatura HMAC do webhook não confere — Meta ou um terceiro tentando forjar uma entrega.\nexport class InvalidWebhookSignatureError extends MetaWhatsAppError {\n constructor() {\n super('Assinatura do webhook inválida.', 401, META_WHATSAPP_ERROR_CODES.INVALID_SIGNATURE)\n }\n}\n\n// Meta reenviou uma entrega dentro da janela de replay (nonce já visto) — não é erro do\n// cliente, é comportamento esperado da Cloud API; o host deve responder 200 e ignorar.\nexport class DuplicateWebhookDeliveryError extends MetaWhatsAppError {\n constructor(nonce: string) {\n super('Entrega de webhook duplicada.', 409, META_WHATSAPP_ERROR_CODES.DUPLICATE_DELIVERY, { nonce })\n }\n}\n\nexport class ConfigMissingError extends MetaWhatsAppError {\n constructor(missingField: string) {\n super(`Configuração do WhatsApp ausente: ${missingField}.`, 503, META_WHATSAPP_ERROR_CODES.CONFIG_MISSING, {\n missingField,\n })\n }\n}\n\nexport class TemplateNotConfiguredError extends MetaWhatsAppError {\n constructor() {\n super(\n 'Nenhum template de WhatsApp configurado para reabertura de janela.',\n 503,\n META_WHATSAPP_ERROR_CODES.TEMPLATE_NOT_CONFIGURED,\n )\n }\n}\n\nexport class SessionNotFoundError extends MetaWhatsAppError {\n constructor(whatsappNumber: string) {\n super(\n `Sessão de conversa não encontrada para ${whatsappNumber}.`,\n 404,\n META_WHATSAPP_ERROR_CODES.SESSION_NOT_FOUND,\n { whatsappNumber },\n )\n }\n}\n"],"mappings":"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","names":["whatsAppMediaSchema","z","object","id","string","mime_type","sha256","optional","caption","filename","whatsAppInteractiveSchema","type","button_reply","title","list_reply","whatsAppOrderSchema","catalog_id","text","product_items","array","product_retailer_id","quantity","number","item_price","currency","whatsAppMessageSchema","from","body","image","audio","video","document","sticker","interactive","order","context","referred_product","timestamp","whatsAppMessageEchoSchema","whatsAppMessageStatusSchema","enum","whatsAppStatusSchema","status","recipient_id","whatsAppWebhookPayloadSchema","entry","changes","value","messaging_product","messages","message_echoes","statuses","metadata","display_phone_number","phone_number_id","import_zod","CROSS_FLOW_PREFIX","isCrossFlowTarget","target","startsWith","crossFlowKey","slice","length","flowNodeNextSchema","z","union","string","object","byAnswer","record","default","flowNodeDataSchema","id","type","enum","contextKey","optional","questionType","question","options","array","tuple","actionKind","actionParams","unknown","directMessage","fallbackMessage","conditionContextKey","conditionOperator","conditionValue","position","x","number","y","next","passthrough","flowGraphNodesSchema","MetaWhatsAppError","Error","message","statusCode","code","details","name","META_WHATSAPP_ERROR_CODES","WINDOW_EXPIRED","INVALID_SIGNATURE","DUPLICATE_DELIVERY","CONFIG_MISSING","TEMPLATE_NOT_CONFIGURED","SESSION_NOT_FOUND","WindowExpiredError","hoursSinceLastMessage","InvalidWebhookSignatureError","DuplicateWebhookDeliveryError","nonce","ConfigMissingError","missingField","TemplateNotConfiguredError","SessionNotFoundError","whatsappNumber"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/webhook.types.ts","../src/flow.types.ts","../src/errors.ts"],"sourcesContent":["export {\n whatsAppMediaSchema,\n whatsAppInteractiveSchema,\n whatsAppOrderSchema,\n whatsAppMessageSchema,\n whatsAppMessageEchoSchema,\n whatsAppMessageStatusSchema,\n whatsAppStatusSchema,\n whatsAppWebhookPayloadSchema,\n} from './webhook.types'\nexport type {\n WhatsAppMedia,\n WhatsAppInteractive,\n WhatsAppOrder,\n WhatsAppMessage,\n WhatsAppMessageEcho,\n WhatsAppMessageStatusValue,\n WhatsAppStatus,\n WhatsAppWebhookPayload,\n} from './webhook.types'\n\nexport type {\n SessionState,\n SessionMode,\n MessageDirection,\n MessageSender,\n MessageStatus,\n ConversationSession,\n ConversationMessage,\n ConversationSummary,\n ListConversationsParams,\n ListMessagesParams,\n} from './conversation.types'\n\nexport {\n CROSS_FLOW_PREFIX,\n FLOW_ACTION_KIND,\n isCrossFlowTarget,\n crossFlowKey,\n flowNodeNextSchema,\n flowNodeDataSchema,\n flowGraphNodesSchema,\n} from './flow.types'\nexport type {\n FlowNodeType,\n FlowQuestionType,\n FlowActionKind,\n FlowConditionOperator,\n FlowNodeNext,\n FlowNodeData,\n FlowGraphData,\n FlowGraphSummary,\n LiveFlowPosition,\n} from './flow.types'\n\nexport type {\n WhatsAppSettings,\n TemplateVariablesMap,\n TemplateConfig,\n WhatsAppTemplateSummary,\n CreateTemplateResult,\n} from './settings.types'\n\nexport type { MetaWhatsAppHooks, MessageHookOutcome, InboundMediaDescriptor } from './events'\n\nexport type {\n ChannelAdapterInterface,\n SubjectResolverInterface,\n CatalogProduct,\n CatalogPort,\n ObjectStorageInterface,\n RealtimeNotifierInterface,\n FlowActionHandler,\n FlowActionResult,\n FlowActionRegistry,\n} from './providers'\n\nexport {\n MetaWhatsAppError,\n META_WHATSAPP_ERROR_CODES,\n WindowExpiredError,\n InvalidWebhookSignatureError,\n DuplicateWebhookDeliveryError,\n ConfigMissingError,\n TemplateNotConfiguredError,\n SessionNotFoundError,\n} from './errors'\n","import { z } from 'zod'\n\n// Extraído de financiamento-imobiliario-bot/apps/api/src/modules/webhook/application/use-cases/\n// ReceiveWhatsAppWebhook.use-case.ts:39-113 — shape real do payload da Cloud API da Meta.\n\nexport const whatsAppMediaSchema = z.object({\n id: z.string(),\n mime_type: z.string(),\n sha256: z.string().optional(),\n caption: z.string().optional(),\n filename: z.string().optional(),\n})\nexport type WhatsAppMedia = z.infer<typeof whatsAppMediaSchema>\n\nexport const whatsAppInteractiveSchema = z.object({\n type: z.string(),\n button_reply: z.object({ id: z.string(), title: z.string() }).optional(),\n list_reply: z.object({ id: z.string(), title: z.string() }).optional(),\n})\nexport type WhatsAppInteractive = z.infer<typeof whatsAppInteractiveSchema>\n\nexport const whatsAppOrderSchema = z.object({\n catalog_id: z.string(),\n text: z.string().optional(),\n product_items: z.array(\n z.object({\n product_retailer_id: z.string(),\n quantity: z.number(),\n item_price: z.number(),\n currency: z.string(),\n }),\n ),\n})\nexport type WhatsAppOrder = z.infer<typeof whatsAppOrderSchema>\n\nexport const whatsAppMessageSchema = z.object({\n id: z.string(),\n from: z.string(),\n type: z.string(),\n text: z.object({ body: z.string() }).optional(),\n image: whatsAppMediaSchema.optional(),\n audio: whatsAppMediaSchema.optional(),\n video: whatsAppMediaSchema.optional(),\n document: whatsAppMediaSchema.optional(),\n sticker: whatsAppMediaSchema.optional(),\n interactive: whatsAppInteractiveSchema.optional(),\n // Pedido enviado pelo carrinho do catálogo (WhatsApp Commerce)\n order: whatsAppOrderSchema.optional(),\n // Presente quando o cliente abre um item do catálogo e manda mensagem pela página do produto\n context: z\n .object({\n from: z.string().optional(),\n id: z.string().optional(),\n referred_product: z.object({ catalog_id: z.string(), product_retailer_id: z.string() }).optional(),\n })\n .optional(),\n timestamp: z.string(),\n})\nexport type WhatsAppMessage = z.infer<typeof whatsAppMessageSchema>\n\nexport const whatsAppMessageEchoSchema = z.object({\n id: z.string(),\n from: z.string(),\n timestamp: z.string(),\n type: z.string(),\n})\nexport type WhatsAppMessageEcho = z.infer<typeof whatsAppMessageEchoSchema>\n\nexport const whatsAppMessageStatusSchema = z.enum(['sent', 'delivered', 'read', 'failed'])\nexport type WhatsAppMessageStatusValue = z.infer<typeof whatsAppMessageStatusSchema>\n\nexport const whatsAppStatusSchema = z.object({\n id: z.string(),\n status: whatsAppMessageStatusSchema,\n timestamp: z.string(),\n recipient_id: z.string().optional(),\n})\nexport type WhatsAppStatus = z.infer<typeof whatsAppStatusSchema>\n\nexport const whatsAppWebhookPayloadSchema = z.object({\n object: z.string(),\n entry: z.array(\n z.object({\n id: z.string(),\n changes: z.array(\n z.object({\n value: z.object({\n messaging_product: z.string(),\n messages: z.array(whatsAppMessageSchema).optional(),\n message_echoes: z.array(whatsAppMessageEchoSchema).optional(),\n statuses: z.array(whatsAppStatusSchema).optional(),\n metadata: z.object({ display_phone_number: z.string(), phone_number_id: z.string() }).optional(),\n }),\n }),\n ),\n }),\n ),\n})\nexport type WhatsAppWebhookPayload = z.infer<typeof whatsAppWebhookPayloadSchema>\n","// Mesmo shape de dados do editor visual (@adatechnology/conversations-ui/flows) — o backend\n// interpreta/persiste o grafo, o frontend edita; os dois lados precisam concordar no formato.\n// `FlowActionKind` é string aberta (não union fechada): o host registra os próprios actions\n// (ex.: 'trigger_simulation' no bot) via registerFlowAction() no módulo (T4.3) — o pacote nunca\n// assume nenhum caso de negócio específico.\nimport { z } from 'zod'\n\nexport type FlowNodeType = 'question' | 'entrada_choice' | 'action' | 'menu' | 'condition'\nexport type FlowQuestionType = 'text' | 'money' | 'date' | 'int' | 'cpf' | 'choice'\nexport type FlowActionKind = string\n\n// Os únicos `actionKind` que o próprio pacote implementa — todo o resto é do host. Ficam aqui, e\n// não no editor, porque o mesmo literal é usado nos dois lados (o editor oferece na paleta, o\n// módulo registra o handler) e duas cópias divergem sem nada acusar.\nexport const FLOW_ACTION_KIND = {\n HANDOFF: 'handoff',\n RATE_LIMITED_HANDOFF: 'rate_limited_handoff',\n SEND_PRODUCT_LIST: 'send_product_list',\n SEND_MEDIA: 'send_media',\n} as const\nexport type FlowConditionOperator = '>' | '>=' | '<' | '<=' | '==' | '!=' | 'contains'\nexport type FlowNodeNext = string | { byAnswer: Record<string, string>; default: string }\n\n// Destinos \"flow:<key>\" são saltos para outro fluxo — mesma convenção usada pelo editor visual\n// (conversations-ui/flows/flowGraph.ts); backend e frontend precisam concordar no formato.\nexport const CROSS_FLOW_PREFIX = 'flow:'\nexport const isCrossFlowTarget = (target: string): boolean => target.startsWith(CROSS_FLOW_PREFIX)\nexport const crossFlowKey = (target: string): string => target.slice(CROSS_FLOW_PREFIX.length)\n\nexport type FlowNodeData = {\n id: string\n type: FlowNodeType\n contextKey?: string\n questionType?: FlowQuestionType\n question?: string\n options?: [string, string][]\n actionKind?: FlowActionKind\n // Parâmetros arbitrários que o nó carrega para o handler registrado em `actionKind`. O pacote\n // nunca os interpreta — é o que permite um nó de ação ser configurado pelo editor sem que o\n // vocabulário do produto (o antigo `simulationTemplate`) vaze para dentro do contrato.\n actionParams?: Record<string, unknown>\n directMessage?: string\n fallbackMessage?: string\n conditionContextKey?: string\n conditionOperator?: FlowConditionOperator\n conditionValue?: string\n position?: { x: number; y: number }\n next?: FlowNodeNext\n}\n\nexport interface FlowGraphData {\n key: string\n label: string\n startNodeId: string\n version: number\n nodes: Record<string, FlowNodeData>\n}\n\nexport interface FlowGraphSummary {\n key: string\n label: string\n nodeCount: number\n showInMenu: boolean\n menuOptionLabel?: string\n updatedAt: string\n}\n\n// Retorno de GetLiveFlowPositions (T4.2) — quantas sessões ativas estão em cada nó agora,\n// usado pelo editor para desenhar o \"liveCount\" nos cards (FlowNodeCardData.liveCount).\nexport interface LiveFlowPosition {\n flowKey: string\n nodeId: string\n count: number\n}\n\n// Validação de runtime do grafo. O `nodes` é um jsonb que entrou pelo editor (dado de origem\n// cliente) e sai do banco como `unknown` — sem parse, um grafo malformado só se manifestaria\n// como comportamento estranho lá dentro do interpretador, longe da causa.\n//\n// Permissivo de propósito em dois pontos: `actionKind` é string aberta (o host registra os\n// seus) e `.passthrough()` deixa passar campos extras — um grafo salvo por uma versão mais nova\n// do editor não pode ficar irrecuperável numa versão mais antiga do módulo.\nexport const flowNodeNextSchema = z.union([\n z.string(),\n z.object({ byAnswer: z.record(z.string(), z.string()), default: z.string() }),\n])\n\nexport const flowNodeDataSchema = z\n .object({\n id: z.string(),\n type: z.enum(['question', 'entrada_choice', 'action', 'menu', 'condition']),\n contextKey: z.string().optional(),\n questionType: z.enum(['text', 'money', 'date', 'int', 'cpf', 'choice']).optional(),\n question: z.string().optional(),\n options: z.array(z.tuple([z.string(), z.string()])).optional(),\n actionKind: z.string().optional(),\n actionParams: z.record(z.string(), z.unknown()).optional(),\n directMessage: z.string().optional(),\n fallbackMessage: z.string().optional(),\n conditionContextKey: z.string().optional(),\n conditionOperator: z.enum(['>', '>=', '<', '<=', '==', '!=', 'contains']).optional(),\n conditionValue: z.string().optional(),\n position: z.object({ x: z.number(), y: z.number() }).optional(),\n next: flowNodeNextSchema.optional(),\n })\n .passthrough()\n\nexport const flowGraphNodesSchema = z.record(z.string(), flowNodeDataSchema)\n","// Hierarquia de erros do módulo — não depende do framework de erros do host (nenhum DomainError\n// importado); o host mapeia estes para o seu próprio formato de resposta no exception filter.\nexport class MetaWhatsAppError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n public readonly code: string,\n public readonly details?: Record<string, unknown>,\n ) {\n super(message)\n this.name = 'MetaWhatsAppError'\n }\n}\n\nexport const META_WHATSAPP_ERROR_CODES = {\n WINDOW_EXPIRED: 'META_WHATSAPP_WINDOW_EXPIRED',\n INVALID_SIGNATURE: 'META_WHATSAPP_INVALID_SIGNATURE',\n DUPLICATE_DELIVERY: 'META_WHATSAPP_DUPLICATE_DELIVERY',\n CONFIG_MISSING: 'META_WHATSAPP_CONFIG_MISSING',\n TEMPLATE_NOT_CONFIGURED: 'META_WHATSAPP_TEMPLATE_NOT_CONFIGURED',\n SESSION_NOT_FOUND: 'META_WHATSAPP_SESSION_NOT_FOUND',\n} as const\n\nexport class WindowExpiredError extends MetaWhatsAppError {\n constructor(public readonly hoursSinceLastMessage?: number) {\n super(\n 'Janela de 24h expirada — não é possível enviar mensagem para este contato fora de template.',\n 400,\n META_WHATSAPP_ERROR_CODES.WINDOW_EXPIRED,\n { hoursSinceLastMessage },\n )\n }\n}\n\n// Assinatura HMAC do webhook não confere — Meta ou um terceiro tentando forjar uma entrega.\nexport class InvalidWebhookSignatureError extends MetaWhatsAppError {\n constructor() {\n super('Assinatura do webhook inválida.', 401, META_WHATSAPP_ERROR_CODES.INVALID_SIGNATURE)\n }\n}\n\n// Meta reenviou uma entrega dentro da janela de replay (nonce já visto) — não é erro do\n// cliente, é comportamento esperado da Cloud API; o host deve responder 200 e ignorar.\nexport class DuplicateWebhookDeliveryError extends MetaWhatsAppError {\n constructor(nonce: string) {\n super('Entrega de webhook duplicada.', 409, META_WHATSAPP_ERROR_CODES.DUPLICATE_DELIVERY, { nonce })\n }\n}\n\nexport class ConfigMissingError extends MetaWhatsAppError {\n constructor(missingField: string) {\n super(`Configuração do WhatsApp ausente: ${missingField}.`, 503, META_WHATSAPP_ERROR_CODES.CONFIG_MISSING, {\n missingField,\n })\n }\n}\n\nexport class TemplateNotConfiguredError extends MetaWhatsAppError {\n constructor() {\n super(\n 'Nenhum template de WhatsApp configurado para reabertura de janela.',\n 503,\n META_WHATSAPP_ERROR_CODES.TEMPLATE_NOT_CONFIGURED,\n )\n }\n}\n\nexport class SessionNotFoundError extends MetaWhatsAppError {\n constructor(whatsappNumber: string) {\n super(\n `Sessão de conversa não encontrada para ${whatsappNumber}.`,\n 404,\n META_WHATSAPP_ERROR_CODES.SESSION_NOT_FOUND,\n { whatsappNumber },\n )\n }\n}\n"],"mappings":"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","names":["whatsAppMediaSchema","z","object","id","string","mime_type","sha256","optional","caption","filename","whatsAppInteractiveSchema","type","button_reply","title","list_reply","whatsAppOrderSchema","catalog_id","text","product_items","array","product_retailer_id","quantity","number","item_price","currency","whatsAppMessageSchema","from","body","image","audio","video","document","sticker","interactive","order","context","referred_product","timestamp","whatsAppMessageEchoSchema","whatsAppMessageStatusSchema","enum","whatsAppStatusSchema","status","recipient_id","whatsAppWebhookPayloadSchema","entry","changes","value","messaging_product","messages","message_echoes","statuses","metadata","display_phone_number","phone_number_id","import_zod","FLOW_ACTION_KIND","HANDOFF","RATE_LIMITED_HANDOFF","SEND_PRODUCT_LIST","SEND_MEDIA","CROSS_FLOW_PREFIX","isCrossFlowTarget","target","startsWith","crossFlowKey","slice","length","flowNodeNextSchema","z","union","string","object","byAnswer","record","default","flowNodeDataSchema","id","type","enum","contextKey","optional","questionType","question","options","array","tuple","actionKind","actionParams","unknown","directMessage","fallbackMessage","conditionContextKey","conditionOperator","conditionValue","position","x","number","y","next","passthrough","flowGraphNodesSchema","MetaWhatsAppError","Error","message","statusCode","code","details","name","META_WHATSAPP_ERROR_CODES","WINDOW_EXPIRED","INVALID_SIGNATURE","DUPLICATE_DELIVERY","CONFIG_MISSING","TEMPLATE_NOT_CONFIGURED","SESSION_NOT_FOUND","WindowExpiredError","hoursSinceLastMessage","InvalidWebhookSignatureError","DuplicateWebhookDeliveryError","nonce","ConfigMissingError","missingField","TemplateNotConfiguredError","SessionNotFoundError","whatsappNumber"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -67,6 +67,12 @@ interface ListMessagesParams {
|
|
|
67
67
|
type FlowNodeType = 'question' | 'entrada_choice' | 'action' | 'menu' | 'condition';
|
|
68
68
|
type FlowQuestionType = 'text' | 'money' | 'date' | 'int' | 'cpf' | 'choice';
|
|
69
69
|
type FlowActionKind = string;
|
|
70
|
+
declare const FLOW_ACTION_KIND: {
|
|
71
|
+
readonly HANDOFF: "handoff";
|
|
72
|
+
readonly RATE_LIMITED_HANDOFF: "rate_limited_handoff";
|
|
73
|
+
readonly SEND_PRODUCT_LIST: "send_product_list";
|
|
74
|
+
readonly SEND_MEDIA: "send_media";
|
|
75
|
+
};
|
|
70
76
|
type FlowConditionOperator = '>' | '>=' | '<' | '<=' | '==' | '!=' | 'contains';
|
|
71
77
|
type FlowNodeNext = string | {
|
|
72
78
|
byAnswer: Record<string, string>;
|
|
@@ -378,7 +384,39 @@ interface MetaWhatsAppHooks {
|
|
|
378
384
|
onStatusUpdate?: (status: WhatsAppStatus, session: ConversationSession | null) => Promise<void>;
|
|
379
385
|
onSessionExpired?: (session: ConversationSession) => Promise<void>;
|
|
380
386
|
onHumanRequested?: (session: ConversationSession) => Promise<void>;
|
|
387
|
+
/**
|
|
388
|
+
* Mídia recebida do cliente, já persistida como mensagem e pronta para ser copiada da Meta para
|
|
389
|
+
* o storage do host — normalmente enfileirando um job.
|
|
390
|
+
*
|
|
391
|
+
* Hook próprio, e não parte do `onMessageReceived`, porque aquele pertence ao fluxo do bot: ele
|
|
392
|
+
* nem é chamado quando a conversa está em atendimento humano, que é exatamente quando o cliente
|
|
393
|
+
* manda documento para o atendente. Ligar ingestão lá perderia esses arquivos em silêncio.
|
|
394
|
+
*
|
|
395
|
+
* A URL de download da Meta expira, então quem implementa deve tratar como trabalho urgente.
|
|
396
|
+
*/
|
|
397
|
+
onMediaReceived?: (media: InboundMediaDescriptor) => Promise<void>;
|
|
398
|
+
/**
|
|
399
|
+
* Falha ao enviar UM arquivo da biblioteca num nó `send_media`.
|
|
400
|
+
*
|
|
401
|
+
* O erro é entregue aqui em vez de propagado porque um PDF que não subiu não pode travar a
|
|
402
|
+
* conversa num nó automático — o cliente ficaria parado sem nenhuma mensagem. O módulo segue
|
|
403
|
+
* para o próximo arquivo e para o próximo nó; observar e alertar é do host.
|
|
404
|
+
*/
|
|
405
|
+
onFlowMediaError?: (error: unknown, details: {
|
|
406
|
+
flowKey: string;
|
|
407
|
+
nodeId: string;
|
|
408
|
+
uploadId: string;
|
|
409
|
+
}) => void;
|
|
381
410
|
}
|
|
411
|
+
type InboundMediaDescriptor = {
|
|
412
|
+
readonly companyId: string;
|
|
413
|
+
readonly messageId: string;
|
|
414
|
+
readonly whatsappNumber: string;
|
|
415
|
+
/** ID da mídia na Meta — a origem do download, que expira. */
|
|
416
|
+
readonly sourceMediaId: string;
|
|
417
|
+
readonly mimeType: string;
|
|
418
|
+
readonly filename?: string;
|
|
419
|
+
};
|
|
382
420
|
|
|
383
421
|
interface ChannelAdapterInterface {
|
|
384
422
|
sendText(to: string, body: string): Promise<{
|
|
@@ -450,7 +488,30 @@ interface ObjectStorageInterface {
|
|
|
450
488
|
}): Promise<{
|
|
451
489
|
uploadId: string;
|
|
452
490
|
}>;
|
|
453
|
-
|
|
491
|
+
/**
|
|
492
|
+
* `options.disposition` decide entre abrir no navegador e salvar. Precisa ser resolvido na
|
|
493
|
+
* criação da URL porque entra na assinatura — depois de assinada, o cliente não muda.
|
|
494
|
+
*/
|
|
495
|
+
getDownloadUrl(uploadId: string, options?: {
|
|
496
|
+
expiresInSeconds?: number;
|
|
497
|
+
disposition?: 'inline' | 'attachment';
|
|
498
|
+
filename?: string;
|
|
499
|
+
}): Promise<string>;
|
|
500
|
+
/**
|
|
501
|
+
* Lê o binário de volta. Opcional pelo mesmo motivo do `delete` — quem já implementa este
|
|
502
|
+
* contrato não pode quebrar ao atualizar o pacote — mas sem ele a action `send_media` fica
|
|
503
|
+
* desligada: reenviar um arquivo da biblioteca ao cliente exige os bytes, e `getDownloadUrl`
|
|
504
|
+
* não serve (a Graph API recebe o conteúdo, não uma URL assinada nossa).
|
|
505
|
+
*/
|
|
506
|
+
getObject?(uploadId: string): Promise<Buffer>;
|
|
507
|
+
/**
|
|
508
|
+
* Apaga o objeto. Opcional para não quebrar quem já implementa este contrato, mas **sem ele não
|
|
509
|
+
* existe exclusão de verdade**: a cascata da FK derruba a linha e deixa o binário órfão no
|
|
510
|
+
* storage, sendo cobrado indefinidamente e sem nada que o alcance.
|
|
511
|
+
*
|
|
512
|
+
* Deve ser idempotente — apagar o que já não existe não é erro, e o job de retenção pode repetir.
|
|
513
|
+
*/
|
|
514
|
+
delete?(uploadId: string): Promise<void>;
|
|
454
515
|
}
|
|
455
516
|
interface RealtimeNotifierInterface {
|
|
456
517
|
emit(channel: string, event: string, payload: Record<string, unknown>): void;
|
|
@@ -503,4 +564,4 @@ declare class SessionNotFoundError extends MetaWhatsAppError {
|
|
|
503
564
|
constructor(whatsappNumber: string);
|
|
504
565
|
}
|
|
505
566
|
|
|
506
|
-
export { CROSS_FLOW_PREFIX, type CatalogPort, type CatalogProduct, type ChannelAdapterInterface, ConfigMissingError, type ConversationMessage, type ConversationSession, type ConversationSummary, type CreateTemplateResult, DuplicateWebhookDeliveryError, type FlowActionHandler, type FlowActionKind, type FlowActionRegistry, type FlowActionResult, type FlowConditionOperator, type FlowGraphData, type FlowGraphSummary, type FlowNodeData, type FlowNodeNext, type FlowNodeType, type FlowQuestionType, InvalidWebhookSignatureError, type ListConversationsParams, type ListMessagesParams, type LiveFlowPosition, META_WHATSAPP_ERROR_CODES, type MessageDirection, type MessageHookOutcome, type MessageSender, type MessageStatus, MetaWhatsAppError, type MetaWhatsAppHooks, type ObjectStorageInterface, type RealtimeNotifierInterface, type SessionMode, SessionNotFoundError, type SessionState, type SubjectResolverInterface, type TemplateConfig, TemplateNotConfiguredError, type TemplateVariablesMap, WhatsAppMessage, type WhatsAppSettings, WhatsAppStatus, type WhatsAppTemplateSummary, WindowExpiredError, crossFlowKey, flowGraphNodesSchema, flowNodeDataSchema, flowNodeNextSchema, isCrossFlowTarget };
|
|
567
|
+
export { CROSS_FLOW_PREFIX, type CatalogPort, type CatalogProduct, type ChannelAdapterInterface, ConfigMissingError, type ConversationMessage, type ConversationSession, type ConversationSummary, type CreateTemplateResult, DuplicateWebhookDeliveryError, FLOW_ACTION_KIND, type FlowActionHandler, type FlowActionKind, type FlowActionRegistry, type FlowActionResult, type FlowConditionOperator, type FlowGraphData, type FlowGraphSummary, type FlowNodeData, type FlowNodeNext, type FlowNodeType, type FlowQuestionType, type InboundMediaDescriptor, InvalidWebhookSignatureError, type ListConversationsParams, type ListMessagesParams, type LiveFlowPosition, META_WHATSAPP_ERROR_CODES, type MessageDirection, type MessageHookOutcome, type MessageSender, type MessageStatus, MetaWhatsAppError, type MetaWhatsAppHooks, type ObjectStorageInterface, type RealtimeNotifierInterface, type SessionMode, SessionNotFoundError, type SessionState, type SubjectResolverInterface, type TemplateConfig, TemplateNotConfiguredError, type TemplateVariablesMap, WhatsAppMessage, type WhatsAppSettings, WhatsAppStatus, type WhatsAppTemplateSummary, WindowExpiredError, crossFlowKey, flowGraphNodesSchema, flowNodeDataSchema, flowNodeNextSchema, isCrossFlowTarget };
|
package/dist/index.d.ts
CHANGED
|
@@ -67,6 +67,12 @@ interface ListMessagesParams {
|
|
|
67
67
|
type FlowNodeType = 'question' | 'entrada_choice' | 'action' | 'menu' | 'condition';
|
|
68
68
|
type FlowQuestionType = 'text' | 'money' | 'date' | 'int' | 'cpf' | 'choice';
|
|
69
69
|
type FlowActionKind = string;
|
|
70
|
+
declare const FLOW_ACTION_KIND: {
|
|
71
|
+
readonly HANDOFF: "handoff";
|
|
72
|
+
readonly RATE_LIMITED_HANDOFF: "rate_limited_handoff";
|
|
73
|
+
readonly SEND_PRODUCT_LIST: "send_product_list";
|
|
74
|
+
readonly SEND_MEDIA: "send_media";
|
|
75
|
+
};
|
|
70
76
|
type FlowConditionOperator = '>' | '>=' | '<' | '<=' | '==' | '!=' | 'contains';
|
|
71
77
|
type FlowNodeNext = string | {
|
|
72
78
|
byAnswer: Record<string, string>;
|
|
@@ -378,7 +384,39 @@ interface MetaWhatsAppHooks {
|
|
|
378
384
|
onStatusUpdate?: (status: WhatsAppStatus, session: ConversationSession | null) => Promise<void>;
|
|
379
385
|
onSessionExpired?: (session: ConversationSession) => Promise<void>;
|
|
380
386
|
onHumanRequested?: (session: ConversationSession) => Promise<void>;
|
|
387
|
+
/**
|
|
388
|
+
* Mídia recebida do cliente, já persistida como mensagem e pronta para ser copiada da Meta para
|
|
389
|
+
* o storage do host — normalmente enfileirando um job.
|
|
390
|
+
*
|
|
391
|
+
* Hook próprio, e não parte do `onMessageReceived`, porque aquele pertence ao fluxo do bot: ele
|
|
392
|
+
* nem é chamado quando a conversa está em atendimento humano, que é exatamente quando o cliente
|
|
393
|
+
* manda documento para o atendente. Ligar ingestão lá perderia esses arquivos em silêncio.
|
|
394
|
+
*
|
|
395
|
+
* A URL de download da Meta expira, então quem implementa deve tratar como trabalho urgente.
|
|
396
|
+
*/
|
|
397
|
+
onMediaReceived?: (media: InboundMediaDescriptor) => Promise<void>;
|
|
398
|
+
/**
|
|
399
|
+
* Falha ao enviar UM arquivo da biblioteca num nó `send_media`.
|
|
400
|
+
*
|
|
401
|
+
* O erro é entregue aqui em vez de propagado porque um PDF que não subiu não pode travar a
|
|
402
|
+
* conversa num nó automático — o cliente ficaria parado sem nenhuma mensagem. O módulo segue
|
|
403
|
+
* para o próximo arquivo e para o próximo nó; observar e alertar é do host.
|
|
404
|
+
*/
|
|
405
|
+
onFlowMediaError?: (error: unknown, details: {
|
|
406
|
+
flowKey: string;
|
|
407
|
+
nodeId: string;
|
|
408
|
+
uploadId: string;
|
|
409
|
+
}) => void;
|
|
381
410
|
}
|
|
411
|
+
type InboundMediaDescriptor = {
|
|
412
|
+
readonly companyId: string;
|
|
413
|
+
readonly messageId: string;
|
|
414
|
+
readonly whatsappNumber: string;
|
|
415
|
+
/** ID da mídia na Meta — a origem do download, que expira. */
|
|
416
|
+
readonly sourceMediaId: string;
|
|
417
|
+
readonly mimeType: string;
|
|
418
|
+
readonly filename?: string;
|
|
419
|
+
};
|
|
382
420
|
|
|
383
421
|
interface ChannelAdapterInterface {
|
|
384
422
|
sendText(to: string, body: string): Promise<{
|
|
@@ -450,7 +488,30 @@ interface ObjectStorageInterface {
|
|
|
450
488
|
}): Promise<{
|
|
451
489
|
uploadId: string;
|
|
452
490
|
}>;
|
|
453
|
-
|
|
491
|
+
/**
|
|
492
|
+
* `options.disposition` decide entre abrir no navegador e salvar. Precisa ser resolvido na
|
|
493
|
+
* criação da URL porque entra na assinatura — depois de assinada, o cliente não muda.
|
|
494
|
+
*/
|
|
495
|
+
getDownloadUrl(uploadId: string, options?: {
|
|
496
|
+
expiresInSeconds?: number;
|
|
497
|
+
disposition?: 'inline' | 'attachment';
|
|
498
|
+
filename?: string;
|
|
499
|
+
}): Promise<string>;
|
|
500
|
+
/**
|
|
501
|
+
* Lê o binário de volta. Opcional pelo mesmo motivo do `delete` — quem já implementa este
|
|
502
|
+
* contrato não pode quebrar ao atualizar o pacote — mas sem ele a action `send_media` fica
|
|
503
|
+
* desligada: reenviar um arquivo da biblioteca ao cliente exige os bytes, e `getDownloadUrl`
|
|
504
|
+
* não serve (a Graph API recebe o conteúdo, não uma URL assinada nossa).
|
|
505
|
+
*/
|
|
506
|
+
getObject?(uploadId: string): Promise<Buffer>;
|
|
507
|
+
/**
|
|
508
|
+
* Apaga o objeto. Opcional para não quebrar quem já implementa este contrato, mas **sem ele não
|
|
509
|
+
* existe exclusão de verdade**: a cascata da FK derruba a linha e deixa o binário órfão no
|
|
510
|
+
* storage, sendo cobrado indefinidamente e sem nada que o alcance.
|
|
511
|
+
*
|
|
512
|
+
* Deve ser idempotente — apagar o que já não existe não é erro, e o job de retenção pode repetir.
|
|
513
|
+
*/
|
|
514
|
+
delete?(uploadId: string): Promise<void>;
|
|
454
515
|
}
|
|
455
516
|
interface RealtimeNotifierInterface {
|
|
456
517
|
emit(channel: string, event: string, payload: Record<string, unknown>): void;
|
|
@@ -503,4 +564,4 @@ declare class SessionNotFoundError extends MetaWhatsAppError {
|
|
|
503
564
|
constructor(whatsappNumber: string);
|
|
504
565
|
}
|
|
505
566
|
|
|
506
|
-
export { CROSS_FLOW_PREFIX, type CatalogPort, type CatalogProduct, type ChannelAdapterInterface, ConfigMissingError, type ConversationMessage, type ConversationSession, type ConversationSummary, type CreateTemplateResult, DuplicateWebhookDeliveryError, type FlowActionHandler, type FlowActionKind, type FlowActionRegistry, type FlowActionResult, type FlowConditionOperator, type FlowGraphData, type FlowGraphSummary, type FlowNodeData, type FlowNodeNext, type FlowNodeType, type FlowQuestionType, InvalidWebhookSignatureError, type ListConversationsParams, type ListMessagesParams, type LiveFlowPosition, META_WHATSAPP_ERROR_CODES, type MessageDirection, type MessageHookOutcome, type MessageSender, type MessageStatus, MetaWhatsAppError, type MetaWhatsAppHooks, type ObjectStorageInterface, type RealtimeNotifierInterface, type SessionMode, SessionNotFoundError, type SessionState, type SubjectResolverInterface, type TemplateConfig, TemplateNotConfiguredError, type TemplateVariablesMap, WhatsAppMessage, type WhatsAppSettings, WhatsAppStatus, type WhatsAppTemplateSummary, WindowExpiredError, crossFlowKey, flowGraphNodesSchema, flowNodeDataSchema, flowNodeNextSchema, isCrossFlowTarget };
|
|
567
|
+
export { CROSS_FLOW_PREFIX, type CatalogPort, type CatalogProduct, type ChannelAdapterInterface, ConfigMissingError, type ConversationMessage, type ConversationSession, type ConversationSummary, type CreateTemplateResult, DuplicateWebhookDeliveryError, FLOW_ACTION_KIND, type FlowActionHandler, type FlowActionKind, type FlowActionRegistry, type FlowActionResult, type FlowConditionOperator, type FlowGraphData, type FlowGraphSummary, type FlowNodeData, type FlowNodeNext, type FlowNodeType, type FlowQuestionType, type InboundMediaDescriptor, InvalidWebhookSignatureError, type ListConversationsParams, type ListMessagesParams, type LiveFlowPosition, META_WHATSAPP_ERROR_CODES, type MessageDirection, type MessageHookOutcome, type MessageSender, type MessageStatus, MetaWhatsAppError, type MetaWhatsAppHooks, type ObjectStorageInterface, type RealtimeNotifierInterface, type SessionMode, SessionNotFoundError, type SessionState, type SubjectResolverInterface, type TemplateConfig, TemplateNotConfiguredError, type TemplateVariablesMap, WhatsAppMessage, type WhatsAppSettings, WhatsAppStatus, type WhatsAppTemplateSummary, WindowExpiredError, crossFlowKey, flowGraphNodesSchema, flowNodeDataSchema, flowNodeNextSchema, isCrossFlowTarget };
|
package/dist/index.js
CHANGED
|
@@ -96,6 +96,12 @@ var whatsAppWebhookPayloadSchema = z.object({
|
|
|
96
96
|
|
|
97
97
|
// src/flow.types.ts
|
|
98
98
|
import { z as z2 } from "zod";
|
|
99
|
+
var FLOW_ACTION_KIND = {
|
|
100
|
+
HANDOFF: "handoff",
|
|
101
|
+
RATE_LIMITED_HANDOFF: "rate_limited_handoff",
|
|
102
|
+
SEND_PRODUCT_LIST: "send_product_list",
|
|
103
|
+
SEND_MEDIA: "send_media"
|
|
104
|
+
};
|
|
99
105
|
var CROSS_FLOW_PREFIX = "flow:";
|
|
100
106
|
var isCrossFlowTarget = /* @__PURE__ */ __name((target) => target.startsWith(CROSS_FLOW_PREFIX), "isCrossFlowTarget");
|
|
101
107
|
var crossFlowKey = /* @__PURE__ */ __name((target) => target.slice(CROSS_FLOW_PREFIX.length), "crossFlowKey");
|
|
@@ -234,6 +240,7 @@ export {
|
|
|
234
240
|
CROSS_FLOW_PREFIX,
|
|
235
241
|
ConfigMissingError,
|
|
236
242
|
DuplicateWebhookDeliveryError,
|
|
243
|
+
FLOW_ACTION_KIND,
|
|
237
244
|
InvalidWebhookSignatureError,
|
|
238
245
|
META_WHATSAPP_ERROR_CODES,
|
|
239
246
|
MetaWhatsAppError,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/webhook.types.ts","../src/flow.types.ts","../src/errors.ts"],"sourcesContent":["import { z } from 'zod'\n\n// Extraído de financiamento-imobiliario-bot/apps/api/src/modules/webhook/application/use-cases/\n// ReceiveWhatsAppWebhook.use-case.ts:39-113 — shape real do payload da Cloud API da Meta.\n\nexport const whatsAppMediaSchema = z.object({\n id: z.string(),\n mime_type: z.string(),\n sha256: z.string().optional(),\n caption: z.string().optional(),\n filename: z.string().optional(),\n})\nexport type WhatsAppMedia = z.infer<typeof whatsAppMediaSchema>\n\nexport const whatsAppInteractiveSchema = z.object({\n type: z.string(),\n button_reply: z.object({ id: z.string(), title: z.string() }).optional(),\n list_reply: z.object({ id: z.string(), title: z.string() }).optional(),\n})\nexport type WhatsAppInteractive = z.infer<typeof whatsAppInteractiveSchema>\n\nexport const whatsAppOrderSchema = z.object({\n catalog_id: z.string(),\n text: z.string().optional(),\n product_items: z.array(\n z.object({\n product_retailer_id: z.string(),\n quantity: z.number(),\n item_price: z.number(),\n currency: z.string(),\n }),\n ),\n})\nexport type WhatsAppOrder = z.infer<typeof whatsAppOrderSchema>\n\nexport const whatsAppMessageSchema = z.object({\n id: z.string(),\n from: z.string(),\n type: z.string(),\n text: z.object({ body: z.string() }).optional(),\n image: whatsAppMediaSchema.optional(),\n audio: whatsAppMediaSchema.optional(),\n video: whatsAppMediaSchema.optional(),\n document: whatsAppMediaSchema.optional(),\n sticker: whatsAppMediaSchema.optional(),\n interactive: whatsAppInteractiveSchema.optional(),\n // Pedido enviado pelo carrinho do catálogo (WhatsApp Commerce)\n order: whatsAppOrderSchema.optional(),\n // Presente quando o cliente abre um item do catálogo e manda mensagem pela página do produto\n context: z\n .object({\n from: z.string().optional(),\n id: z.string().optional(),\n referred_product: z.object({ catalog_id: z.string(), product_retailer_id: z.string() }).optional(),\n })\n .optional(),\n timestamp: z.string(),\n})\nexport type WhatsAppMessage = z.infer<typeof whatsAppMessageSchema>\n\nexport const whatsAppMessageEchoSchema = z.object({\n id: z.string(),\n from: z.string(),\n timestamp: z.string(),\n type: z.string(),\n})\nexport type WhatsAppMessageEcho = z.infer<typeof whatsAppMessageEchoSchema>\n\nexport const whatsAppMessageStatusSchema = z.enum(['sent', 'delivered', 'read', 'failed'])\nexport type WhatsAppMessageStatusValue = z.infer<typeof whatsAppMessageStatusSchema>\n\nexport const whatsAppStatusSchema = z.object({\n id: z.string(),\n status: whatsAppMessageStatusSchema,\n timestamp: z.string(),\n recipient_id: z.string().optional(),\n})\nexport type WhatsAppStatus = z.infer<typeof whatsAppStatusSchema>\n\nexport const whatsAppWebhookPayloadSchema = z.object({\n object: z.string(),\n entry: z.array(\n z.object({\n id: z.string(),\n changes: z.array(\n z.object({\n value: z.object({\n messaging_product: z.string(),\n messages: z.array(whatsAppMessageSchema).optional(),\n message_echoes: z.array(whatsAppMessageEchoSchema).optional(),\n statuses: z.array(whatsAppStatusSchema).optional(),\n metadata: z.object({ display_phone_number: z.string(), phone_number_id: z.string() }).optional(),\n }),\n }),\n ),\n }),\n ),\n})\nexport type WhatsAppWebhookPayload = z.infer<typeof whatsAppWebhookPayloadSchema>\n","// Mesmo shape de dados do editor visual (@adatechnology/conversations-ui/flows) — o backend\n// interpreta/persiste o grafo, o frontend edita; os dois lados precisam concordar no formato.\n// `FlowActionKind` é string aberta (não union fechada): o host registra os próprios actions\n// (ex.: 'trigger_simulation' no bot) via registerFlowAction() no módulo (T4.3) — o pacote nunca\n// assume nenhum caso de negócio específico.\nimport { z } from 'zod'\n\nexport type FlowNodeType = 'question' | 'entrada_choice' | 'action' | 'menu' | 'condition'\nexport type FlowQuestionType = 'text' | 'money' | 'date' | 'int' | 'cpf' | 'choice'\nexport type FlowActionKind = string\nexport type FlowConditionOperator = '>' | '>=' | '<' | '<=' | '==' | '!=' | 'contains'\nexport type FlowNodeNext = string | { byAnswer: Record<string, string>; default: string }\n\n// Destinos \"flow:<key>\" são saltos para outro fluxo — mesma convenção usada pelo editor visual\n// (conversations-ui/flows/flowGraph.ts); backend e frontend precisam concordar no formato.\nexport const CROSS_FLOW_PREFIX = 'flow:'\nexport const isCrossFlowTarget = (target: string): boolean => target.startsWith(CROSS_FLOW_PREFIX)\nexport const crossFlowKey = (target: string): string => target.slice(CROSS_FLOW_PREFIX.length)\n\nexport type FlowNodeData = {\n id: string\n type: FlowNodeType\n contextKey?: string\n questionType?: FlowQuestionType\n question?: string\n options?: [string, string][]\n actionKind?: FlowActionKind\n // Parâmetros arbitrários que o nó carrega para o handler registrado em `actionKind`. O pacote\n // nunca os interpreta — é o que permite um nó de ação ser configurado pelo editor sem que o\n // vocabulário do produto (o antigo `simulationTemplate`) vaze para dentro do contrato.\n actionParams?: Record<string, unknown>\n directMessage?: string\n fallbackMessage?: string\n conditionContextKey?: string\n conditionOperator?: FlowConditionOperator\n conditionValue?: string\n position?: { x: number; y: number }\n next?: FlowNodeNext\n}\n\nexport interface FlowGraphData {\n key: string\n label: string\n startNodeId: string\n version: number\n nodes: Record<string, FlowNodeData>\n}\n\nexport interface FlowGraphSummary {\n key: string\n label: string\n nodeCount: number\n showInMenu: boolean\n menuOptionLabel?: string\n updatedAt: string\n}\n\n// Retorno de GetLiveFlowPositions (T4.2) — quantas sessões ativas estão em cada nó agora,\n// usado pelo editor para desenhar o \"liveCount\" nos cards (FlowNodeCardData.liveCount).\nexport interface LiveFlowPosition {\n flowKey: string\n nodeId: string\n count: number\n}\n\n// Validação de runtime do grafo. O `nodes` é um jsonb que entrou pelo editor (dado de origem\n// cliente) e sai do banco como `unknown` — sem parse, um grafo malformado só se manifestaria\n// como comportamento estranho lá dentro do interpretador, longe da causa.\n//\n// Permissivo de propósito em dois pontos: `actionKind` é string aberta (o host registra os\n// seus) e `.passthrough()` deixa passar campos extras — um grafo salvo por uma versão mais nova\n// do editor não pode ficar irrecuperável numa versão mais antiga do módulo.\nexport const flowNodeNextSchema = z.union([\n z.string(),\n z.object({ byAnswer: z.record(z.string(), z.string()), default: z.string() }),\n])\n\nexport const flowNodeDataSchema = z\n .object({\n id: z.string(),\n type: z.enum(['question', 'entrada_choice', 'action', 'menu', 'condition']),\n contextKey: z.string().optional(),\n questionType: z.enum(['text', 'money', 'date', 'int', 'cpf', 'choice']).optional(),\n question: z.string().optional(),\n options: z.array(z.tuple([z.string(), z.string()])).optional(),\n actionKind: z.string().optional(),\n actionParams: z.record(z.string(), z.unknown()).optional(),\n directMessage: z.string().optional(),\n fallbackMessage: z.string().optional(),\n conditionContextKey: z.string().optional(),\n conditionOperator: z.enum(['>', '>=', '<', '<=', '==', '!=', 'contains']).optional(),\n conditionValue: z.string().optional(),\n position: z.object({ x: z.number(), y: z.number() }).optional(),\n next: flowNodeNextSchema.optional(),\n })\n .passthrough()\n\nexport const flowGraphNodesSchema = z.record(z.string(), flowNodeDataSchema)\n","// Hierarquia de erros do módulo — não depende do framework de erros do host (nenhum DomainError\n// importado); o host mapeia estes para o seu próprio formato de resposta no exception filter.\nexport class MetaWhatsAppError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n public readonly code: string,\n public readonly details?: Record<string, unknown>,\n ) {\n super(message)\n this.name = 'MetaWhatsAppError'\n }\n}\n\nexport const META_WHATSAPP_ERROR_CODES = {\n WINDOW_EXPIRED: 'META_WHATSAPP_WINDOW_EXPIRED',\n INVALID_SIGNATURE: 'META_WHATSAPP_INVALID_SIGNATURE',\n DUPLICATE_DELIVERY: 'META_WHATSAPP_DUPLICATE_DELIVERY',\n CONFIG_MISSING: 'META_WHATSAPP_CONFIG_MISSING',\n TEMPLATE_NOT_CONFIGURED: 'META_WHATSAPP_TEMPLATE_NOT_CONFIGURED',\n SESSION_NOT_FOUND: 'META_WHATSAPP_SESSION_NOT_FOUND',\n} as const\n\nexport class WindowExpiredError extends MetaWhatsAppError {\n constructor(public readonly hoursSinceLastMessage?: number) {\n super(\n 'Janela de 24h expirada — não é possível enviar mensagem para este contato fora de template.',\n 400,\n META_WHATSAPP_ERROR_CODES.WINDOW_EXPIRED,\n { hoursSinceLastMessage },\n )\n }\n}\n\n// Assinatura HMAC do webhook não confere — Meta ou um terceiro tentando forjar uma entrega.\nexport class InvalidWebhookSignatureError extends MetaWhatsAppError {\n constructor() {\n super('Assinatura do webhook inválida.', 401, META_WHATSAPP_ERROR_CODES.INVALID_SIGNATURE)\n }\n}\n\n// Meta reenviou uma entrega dentro da janela de replay (nonce já visto) — não é erro do\n// cliente, é comportamento esperado da Cloud API; o host deve responder 200 e ignorar.\nexport class DuplicateWebhookDeliveryError extends MetaWhatsAppError {\n constructor(nonce: string) {\n super('Entrega de webhook duplicada.', 409, META_WHATSAPP_ERROR_CODES.DUPLICATE_DELIVERY, { nonce })\n }\n}\n\nexport class ConfigMissingError extends MetaWhatsAppError {\n constructor(missingField: string) {\n super(`Configuração do WhatsApp ausente: ${missingField}.`, 503, META_WHATSAPP_ERROR_CODES.CONFIG_MISSING, {\n missingField,\n })\n }\n}\n\nexport class TemplateNotConfiguredError extends MetaWhatsAppError {\n constructor() {\n super(\n 'Nenhum template de WhatsApp configurado para reabertura de janela.',\n 503,\n META_WHATSAPP_ERROR_CODES.TEMPLATE_NOT_CONFIGURED,\n )\n }\n}\n\nexport class SessionNotFoundError extends MetaWhatsAppError {\n constructor(whatsappNumber: string) {\n super(\n `Sessão de conversa não encontrada para ${whatsappNumber}.`,\n 404,\n META_WHATSAPP_ERROR_CODES.SESSION_NOT_FOUND,\n { whatsappNumber },\n )\n }\n}\n"],"mappings":"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","names":["z","whatsAppMediaSchema","object","id","string","mime_type","sha256","optional","caption","filename","whatsAppInteractiveSchema","type","button_reply","title","list_reply","whatsAppOrderSchema","catalog_id","text","product_items","array","product_retailer_id","quantity","number","item_price","currency","whatsAppMessageSchema","from","body","image","audio","video","document","sticker","interactive","order","context","referred_product","timestamp","whatsAppMessageEchoSchema","whatsAppMessageStatusSchema","enum","whatsAppStatusSchema","status","recipient_id","whatsAppWebhookPayloadSchema","entry","changes","value","messaging_product","messages","message_echoes","statuses","metadata","display_phone_number","phone_number_id","z","CROSS_FLOW_PREFIX","isCrossFlowTarget","target","startsWith","crossFlowKey","slice","length","flowNodeNextSchema","z","union","string","object","byAnswer","record","default","flowNodeDataSchema","id","type","enum","contextKey","optional","questionType","question","options","array","tuple","actionKind","actionParams","unknown","directMessage","fallbackMessage","conditionContextKey","conditionOperator","conditionValue","position","x","number","y","next","passthrough","flowGraphNodesSchema","MetaWhatsAppError","Error","message","statusCode","code","details","name","META_WHATSAPP_ERROR_CODES","WINDOW_EXPIRED","INVALID_SIGNATURE","DUPLICATE_DELIVERY","CONFIG_MISSING","TEMPLATE_NOT_CONFIGURED","SESSION_NOT_FOUND","WindowExpiredError","hoursSinceLastMessage","InvalidWebhookSignatureError","DuplicateWebhookDeliveryError","nonce","ConfigMissingError","missingField","TemplateNotConfiguredError","SessionNotFoundError","whatsappNumber"]}
|
|
1
|
+
{"version":3,"sources":["../src/webhook.types.ts","../src/flow.types.ts","../src/errors.ts"],"sourcesContent":["import { z } from 'zod'\n\n// Extraído de financiamento-imobiliario-bot/apps/api/src/modules/webhook/application/use-cases/\n// ReceiveWhatsAppWebhook.use-case.ts:39-113 — shape real do payload da Cloud API da Meta.\n\nexport const whatsAppMediaSchema = z.object({\n id: z.string(),\n mime_type: z.string(),\n sha256: z.string().optional(),\n caption: z.string().optional(),\n filename: z.string().optional(),\n})\nexport type WhatsAppMedia = z.infer<typeof whatsAppMediaSchema>\n\nexport const whatsAppInteractiveSchema = z.object({\n type: z.string(),\n button_reply: z.object({ id: z.string(), title: z.string() }).optional(),\n list_reply: z.object({ id: z.string(), title: z.string() }).optional(),\n})\nexport type WhatsAppInteractive = z.infer<typeof whatsAppInteractiveSchema>\n\nexport const whatsAppOrderSchema = z.object({\n catalog_id: z.string(),\n text: z.string().optional(),\n product_items: z.array(\n z.object({\n product_retailer_id: z.string(),\n quantity: z.number(),\n item_price: z.number(),\n currency: z.string(),\n }),\n ),\n})\nexport type WhatsAppOrder = z.infer<typeof whatsAppOrderSchema>\n\nexport const whatsAppMessageSchema = z.object({\n id: z.string(),\n from: z.string(),\n type: z.string(),\n text: z.object({ body: z.string() }).optional(),\n image: whatsAppMediaSchema.optional(),\n audio: whatsAppMediaSchema.optional(),\n video: whatsAppMediaSchema.optional(),\n document: whatsAppMediaSchema.optional(),\n sticker: whatsAppMediaSchema.optional(),\n interactive: whatsAppInteractiveSchema.optional(),\n // Pedido enviado pelo carrinho do catálogo (WhatsApp Commerce)\n order: whatsAppOrderSchema.optional(),\n // Presente quando o cliente abre um item do catálogo e manda mensagem pela página do produto\n context: z\n .object({\n from: z.string().optional(),\n id: z.string().optional(),\n referred_product: z.object({ catalog_id: z.string(), product_retailer_id: z.string() }).optional(),\n })\n .optional(),\n timestamp: z.string(),\n})\nexport type WhatsAppMessage = z.infer<typeof whatsAppMessageSchema>\n\nexport const whatsAppMessageEchoSchema = z.object({\n id: z.string(),\n from: z.string(),\n timestamp: z.string(),\n type: z.string(),\n})\nexport type WhatsAppMessageEcho = z.infer<typeof whatsAppMessageEchoSchema>\n\nexport const whatsAppMessageStatusSchema = z.enum(['sent', 'delivered', 'read', 'failed'])\nexport type WhatsAppMessageStatusValue = z.infer<typeof whatsAppMessageStatusSchema>\n\nexport const whatsAppStatusSchema = z.object({\n id: z.string(),\n status: whatsAppMessageStatusSchema,\n timestamp: z.string(),\n recipient_id: z.string().optional(),\n})\nexport type WhatsAppStatus = z.infer<typeof whatsAppStatusSchema>\n\nexport const whatsAppWebhookPayloadSchema = z.object({\n object: z.string(),\n entry: z.array(\n z.object({\n id: z.string(),\n changes: z.array(\n z.object({\n value: z.object({\n messaging_product: z.string(),\n messages: z.array(whatsAppMessageSchema).optional(),\n message_echoes: z.array(whatsAppMessageEchoSchema).optional(),\n statuses: z.array(whatsAppStatusSchema).optional(),\n metadata: z.object({ display_phone_number: z.string(), phone_number_id: z.string() }).optional(),\n }),\n }),\n ),\n }),\n ),\n})\nexport type WhatsAppWebhookPayload = z.infer<typeof whatsAppWebhookPayloadSchema>\n","// Mesmo shape de dados do editor visual (@adatechnology/conversations-ui/flows) — o backend\n// interpreta/persiste o grafo, o frontend edita; os dois lados precisam concordar no formato.\n// `FlowActionKind` é string aberta (não union fechada): o host registra os próprios actions\n// (ex.: 'trigger_simulation' no bot) via registerFlowAction() no módulo (T4.3) — o pacote nunca\n// assume nenhum caso de negócio específico.\nimport { z } from 'zod'\n\nexport type FlowNodeType = 'question' | 'entrada_choice' | 'action' | 'menu' | 'condition'\nexport type FlowQuestionType = 'text' | 'money' | 'date' | 'int' | 'cpf' | 'choice'\nexport type FlowActionKind = string\n\n// Os únicos `actionKind` que o próprio pacote implementa — todo o resto é do host. Ficam aqui, e\n// não no editor, porque o mesmo literal é usado nos dois lados (o editor oferece na paleta, o\n// módulo registra o handler) e duas cópias divergem sem nada acusar.\nexport const FLOW_ACTION_KIND = {\n HANDOFF: 'handoff',\n RATE_LIMITED_HANDOFF: 'rate_limited_handoff',\n SEND_PRODUCT_LIST: 'send_product_list',\n SEND_MEDIA: 'send_media',\n} as const\nexport type FlowConditionOperator = '>' | '>=' | '<' | '<=' | '==' | '!=' | 'contains'\nexport type FlowNodeNext = string | { byAnswer: Record<string, string>; default: string }\n\n// Destinos \"flow:<key>\" são saltos para outro fluxo — mesma convenção usada pelo editor visual\n// (conversations-ui/flows/flowGraph.ts); backend e frontend precisam concordar no formato.\nexport const CROSS_FLOW_PREFIX = 'flow:'\nexport const isCrossFlowTarget = (target: string): boolean => target.startsWith(CROSS_FLOW_PREFIX)\nexport const crossFlowKey = (target: string): string => target.slice(CROSS_FLOW_PREFIX.length)\n\nexport type FlowNodeData = {\n id: string\n type: FlowNodeType\n contextKey?: string\n questionType?: FlowQuestionType\n question?: string\n options?: [string, string][]\n actionKind?: FlowActionKind\n // Parâmetros arbitrários que o nó carrega para o handler registrado em `actionKind`. O pacote\n // nunca os interpreta — é o que permite um nó de ação ser configurado pelo editor sem que o\n // vocabulário do produto (o antigo `simulationTemplate`) vaze para dentro do contrato.\n actionParams?: Record<string, unknown>\n directMessage?: string\n fallbackMessage?: string\n conditionContextKey?: string\n conditionOperator?: FlowConditionOperator\n conditionValue?: string\n position?: { x: number; y: number }\n next?: FlowNodeNext\n}\n\nexport interface FlowGraphData {\n key: string\n label: string\n startNodeId: string\n version: number\n nodes: Record<string, FlowNodeData>\n}\n\nexport interface FlowGraphSummary {\n key: string\n label: string\n nodeCount: number\n showInMenu: boolean\n menuOptionLabel?: string\n updatedAt: string\n}\n\n// Retorno de GetLiveFlowPositions (T4.2) — quantas sessões ativas estão em cada nó agora,\n// usado pelo editor para desenhar o \"liveCount\" nos cards (FlowNodeCardData.liveCount).\nexport interface LiveFlowPosition {\n flowKey: string\n nodeId: string\n count: number\n}\n\n// Validação de runtime do grafo. O `nodes` é um jsonb que entrou pelo editor (dado de origem\n// cliente) e sai do banco como `unknown` — sem parse, um grafo malformado só se manifestaria\n// como comportamento estranho lá dentro do interpretador, longe da causa.\n//\n// Permissivo de propósito em dois pontos: `actionKind` é string aberta (o host registra os\n// seus) e `.passthrough()` deixa passar campos extras — um grafo salvo por uma versão mais nova\n// do editor não pode ficar irrecuperável numa versão mais antiga do módulo.\nexport const flowNodeNextSchema = z.union([\n z.string(),\n z.object({ byAnswer: z.record(z.string(), z.string()), default: z.string() }),\n])\n\nexport const flowNodeDataSchema = z\n .object({\n id: z.string(),\n type: z.enum(['question', 'entrada_choice', 'action', 'menu', 'condition']),\n contextKey: z.string().optional(),\n questionType: z.enum(['text', 'money', 'date', 'int', 'cpf', 'choice']).optional(),\n question: z.string().optional(),\n options: z.array(z.tuple([z.string(), z.string()])).optional(),\n actionKind: z.string().optional(),\n actionParams: z.record(z.string(), z.unknown()).optional(),\n directMessage: z.string().optional(),\n fallbackMessage: z.string().optional(),\n conditionContextKey: z.string().optional(),\n conditionOperator: z.enum(['>', '>=', '<', '<=', '==', '!=', 'contains']).optional(),\n conditionValue: z.string().optional(),\n position: z.object({ x: z.number(), y: z.number() }).optional(),\n next: flowNodeNextSchema.optional(),\n })\n .passthrough()\n\nexport const flowGraphNodesSchema = z.record(z.string(), flowNodeDataSchema)\n","// Hierarquia de erros do módulo — não depende do framework de erros do host (nenhum DomainError\n// importado); o host mapeia estes para o seu próprio formato de resposta no exception filter.\nexport class MetaWhatsAppError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n public readonly code: string,\n public readonly details?: Record<string, unknown>,\n ) {\n super(message)\n this.name = 'MetaWhatsAppError'\n }\n}\n\nexport const META_WHATSAPP_ERROR_CODES = {\n WINDOW_EXPIRED: 'META_WHATSAPP_WINDOW_EXPIRED',\n INVALID_SIGNATURE: 'META_WHATSAPP_INVALID_SIGNATURE',\n DUPLICATE_DELIVERY: 'META_WHATSAPP_DUPLICATE_DELIVERY',\n CONFIG_MISSING: 'META_WHATSAPP_CONFIG_MISSING',\n TEMPLATE_NOT_CONFIGURED: 'META_WHATSAPP_TEMPLATE_NOT_CONFIGURED',\n SESSION_NOT_FOUND: 'META_WHATSAPP_SESSION_NOT_FOUND',\n} as const\n\nexport class WindowExpiredError extends MetaWhatsAppError {\n constructor(public readonly hoursSinceLastMessage?: number) {\n super(\n 'Janela de 24h expirada — não é possível enviar mensagem para este contato fora de template.',\n 400,\n META_WHATSAPP_ERROR_CODES.WINDOW_EXPIRED,\n { hoursSinceLastMessage },\n )\n }\n}\n\n// Assinatura HMAC do webhook não confere — Meta ou um terceiro tentando forjar uma entrega.\nexport class InvalidWebhookSignatureError extends MetaWhatsAppError {\n constructor() {\n super('Assinatura do webhook inválida.', 401, META_WHATSAPP_ERROR_CODES.INVALID_SIGNATURE)\n }\n}\n\n// Meta reenviou uma entrega dentro da janela de replay (nonce já visto) — não é erro do\n// cliente, é comportamento esperado da Cloud API; o host deve responder 200 e ignorar.\nexport class DuplicateWebhookDeliveryError extends MetaWhatsAppError {\n constructor(nonce: string) {\n super('Entrega de webhook duplicada.', 409, META_WHATSAPP_ERROR_CODES.DUPLICATE_DELIVERY, { nonce })\n }\n}\n\nexport class ConfigMissingError extends MetaWhatsAppError {\n constructor(missingField: string) {\n super(`Configuração do WhatsApp ausente: ${missingField}.`, 503, META_WHATSAPP_ERROR_CODES.CONFIG_MISSING, {\n missingField,\n })\n }\n}\n\nexport class TemplateNotConfiguredError extends MetaWhatsAppError {\n constructor() {\n super(\n 'Nenhum template de WhatsApp configurado para reabertura de janela.',\n 503,\n META_WHATSAPP_ERROR_CODES.TEMPLATE_NOT_CONFIGURED,\n )\n }\n}\n\nexport class SessionNotFoundError extends MetaWhatsAppError {\n constructor(whatsappNumber: string) {\n super(\n `Sessão de conversa não encontrada para ${whatsappNumber}.`,\n 404,\n META_WHATSAPP_ERROR_CODES.SESSION_NOT_FOUND,\n { whatsappNumber },\n )\n }\n}\n"],"mappings":";;;;AAAA,SAASA,SAAS;AAKX,IAAMC,sBAAsBD,EAAEE,OAAO;EAC1CC,IAAIH,EAAEI,OAAM;EACZC,WAAWL,EAAEI,OAAM;EACnBE,QAAQN,EAAEI,OAAM,EAAGG,SAAQ;EAC3BC,SAASR,EAAEI,OAAM,EAAGG,SAAQ;EAC5BE,UAAUT,EAAEI,OAAM,EAAGG,SAAQ;AAC/B,CAAA;AAGO,IAAMG,4BAA4BV,EAAEE,OAAO;EAChDS,MAAMX,EAAEI,OAAM;EACdQ,cAAcZ,EAAEE,OAAO;IAAEC,IAAIH,EAAEI,OAAM;IAAIS,OAAOb,EAAEI,OAAM;EAAG,CAAA,EAAGG,SAAQ;EACtEO,YAAYd,EAAEE,OAAO;IAAEC,IAAIH,EAAEI,OAAM;IAAIS,OAAOb,EAAEI,OAAM;EAAG,CAAA,EAAGG,SAAQ;AACtE,CAAA;AAGO,IAAMQ,sBAAsBf,EAAEE,OAAO;EAC1Cc,YAAYhB,EAAEI,OAAM;EACpBa,MAAMjB,EAAEI,OAAM,EAAGG,SAAQ;EACzBW,eAAelB,EAAEmB,MACfnB,EAAEE,OAAO;IACPkB,qBAAqBpB,EAAEI,OAAM;IAC7BiB,UAAUrB,EAAEsB,OAAM;IAClBC,YAAYvB,EAAEsB,OAAM;IACpBE,UAAUxB,EAAEI,OAAM;EACpB,CAAA,CAAA;AAEJ,CAAA;AAGO,IAAMqB,wBAAwBzB,EAAEE,OAAO;EAC5CC,IAAIH,EAAEI,OAAM;EACZsB,MAAM1B,EAAEI,OAAM;EACdO,MAAMX,EAAEI,OAAM;EACda,MAAMjB,EAAEE,OAAO;IAAEyB,MAAM3B,EAAEI,OAAM;EAAG,CAAA,EAAGG,SAAQ;EAC7CqB,OAAO3B,oBAAoBM,SAAQ;EACnCsB,OAAO5B,oBAAoBM,SAAQ;EACnCuB,OAAO7B,oBAAoBM,SAAQ;EACnCwB,UAAU9B,oBAAoBM,SAAQ;EACtCyB,SAAS/B,oBAAoBM,SAAQ;EACrC0B,aAAavB,0BAA0BH,SAAQ;;EAE/C2B,OAAOnB,oBAAoBR,SAAQ;;EAEnC4B,SAASnC,EACNE,OAAO;IACNwB,MAAM1B,EAAEI,OAAM,EAAGG,SAAQ;IACzBJ,IAAIH,EAAEI,OAAM,EAAGG,SAAQ;IACvB6B,kBAAkBpC,EAAEE,OAAO;MAAEc,YAAYhB,EAAEI,OAAM;MAAIgB,qBAAqBpB,EAAEI,OAAM;IAAG,CAAA,EAAGG,SAAQ;EAClG,CAAA,EACCA,SAAQ;EACX8B,WAAWrC,EAAEI,OAAM;AACrB,CAAA;AAGO,IAAMkC,4BAA4BtC,EAAEE,OAAO;EAChDC,IAAIH,EAAEI,OAAM;EACZsB,MAAM1B,EAAEI,OAAM;EACdiC,WAAWrC,EAAEI,OAAM;EACnBO,MAAMX,EAAEI,OAAM;AAChB,CAAA;AAGO,IAAMmC,8BAA8BvC,EAAEwC,KAAK;EAAC;EAAQ;EAAa;EAAQ;CAAS;AAGlF,IAAMC,uBAAuBzC,EAAEE,OAAO;EAC3CC,IAAIH,EAAEI,OAAM;EACZsC,QAAQH;EACRF,WAAWrC,EAAEI,OAAM;EACnBuC,cAAc3C,EAAEI,OAAM,EAAGG,SAAQ;AACnC,CAAA;AAGO,IAAMqC,+BAA+B5C,EAAEE,OAAO;EACnDA,QAAQF,EAAEI,OAAM;EAChByC,OAAO7C,EAAEmB,MACPnB,EAAEE,OAAO;IACPC,IAAIH,EAAEI,OAAM;IACZ0C,SAAS9C,EAAEmB,MACTnB,EAAEE,OAAO;MACP6C,OAAO/C,EAAEE,OAAO;QACd8C,mBAAmBhD,EAAEI,OAAM;QAC3B6C,UAAUjD,EAAEmB,MAAMM,qBAAAA,EAAuBlB,SAAQ;QACjD2C,gBAAgBlD,EAAEmB,MAAMmB,yBAAAA,EAA2B/B,SAAQ;QAC3D4C,UAAUnD,EAAEmB,MAAMsB,oBAAAA,EAAsBlC,SAAQ;QAChD6C,UAAUpD,EAAEE,OAAO;UAAEmD,sBAAsBrD,EAAEI,OAAM;UAAIkD,iBAAiBtD,EAAEI,OAAM;QAAG,CAAA,EAAGG,SAAQ;MAChG,CAAA;IACF,CAAA,CAAA;EAEJ,CAAA,CAAA;AAEJ,CAAA;;;AC5FA,SAASgD,KAAAA,UAAS;AASX,IAAMC,mBAAmB;EAC9BC,SAAS;EACTC,sBAAsB;EACtBC,mBAAmB;EACnBC,YAAY;AACd;AAMO,IAAMC,oBAAoB;AAC1B,IAAMC,oBAAoB,wBAACC,WAA4BA,OAAOC,WAAWH,iBAAAA,GAA/C;AAC1B,IAAMI,eAAe,wBAACF,WAA2BA,OAAOG,MAAML,kBAAkBM,MAAM,GAAjE;AAuDrB,IAAMC,qBAAqBC,GAAEC,MAAM;EACxCD,GAAEE,OAAM;EACRF,GAAEG,OAAO;IAAEC,UAAUJ,GAAEK,OAAOL,GAAEE,OAAM,GAAIF,GAAEE,OAAM,CAAA;IAAKI,SAASN,GAAEE,OAAM;EAAG,CAAA;CAC5E;AAEM,IAAMK,qBAAqBP,GAC/BG,OAAO;EACNK,IAAIR,GAAEE,OAAM;EACZO,MAAMT,GAAEU,KAAK;IAAC;IAAY;IAAkB;IAAU;IAAQ;GAAY;EAC1EC,YAAYX,GAAEE,OAAM,EAAGU,SAAQ;EAC/BC,cAAcb,GAAEU,KAAK;IAAC;IAAQ;IAAS;IAAQ;IAAO;IAAO;GAAS,EAAEE,SAAQ;EAChFE,UAAUd,GAAEE,OAAM,EAAGU,SAAQ;EAC7BG,SAASf,GAAEgB,MAAMhB,GAAEiB,MAAM;IAACjB,GAAEE,OAAM;IAAIF,GAAEE,OAAM;GAAG,CAAA,EAAGU,SAAQ;EAC5DM,YAAYlB,GAAEE,OAAM,EAAGU,SAAQ;EAC/BO,cAAcnB,GAAEK,OAAOL,GAAEE,OAAM,GAAIF,GAAEoB,QAAO,CAAA,EAAIR,SAAQ;EACxDS,eAAerB,GAAEE,OAAM,EAAGU,SAAQ;EAClCU,iBAAiBtB,GAAEE,OAAM,EAAGU,SAAQ;EACpCW,qBAAqBvB,GAAEE,OAAM,EAAGU,SAAQ;EACxCY,mBAAmBxB,GAAEU,KAAK;IAAC;IAAK;IAAM;IAAK;IAAM;IAAM;IAAM;GAAW,EAAEE,SAAQ;EAClFa,gBAAgBzB,GAAEE,OAAM,EAAGU,SAAQ;EACnCc,UAAU1B,GAAEG,OAAO;IAAEwB,GAAG3B,GAAE4B,OAAM;IAAIC,GAAG7B,GAAE4B,OAAM;EAAG,CAAA,EAAGhB,SAAQ;EAC7DkB,MAAM/B,mBAAmBa,SAAQ;AACnC,CAAA,EACCmB,YAAW;AAEP,IAAMC,uBAAuBhC,GAAEK,OAAOL,GAAEE,OAAM,GAAIK,kBAAAA;;;ACzGlD,IAAM0B,oBAAN,cAAgCC,MAAAA;EAFvC,OAEuCA;;;;;;EACrC,YACEC,SACgBC,YACAC,MACAC,SAChB;AACA,UAAMH,OAAAA,GAAAA,KAJUC,aAAAA,YAAAA,KACAC,OAAAA,MAAAA,KACAC,UAAAA;AAGhB,SAAKC,OAAO;EACd;AACF;AAEO,IAAMC,4BAA4B;EACvCC,gBAAgB;EAChBC,mBAAmB;EACnBC,oBAAoB;EACpBC,gBAAgB;EAChBC,yBAAyB;EACzBC,mBAAmB;AACrB;AAEO,IAAMC,qBAAN,cAAiCd,kBAAAA;EAvBxC,OAuBwCA;;;;EACtC,YAA4Be,uBAAgC;AAC1D,UACE,6GACA,KACAR,0BAA0BC,gBAC1B;MAAEO;IAAsB,CAAA,GAAA,KALAA,wBAAAA;EAO5B;AACF;AAGO,IAAMC,+BAAN,cAA2ChB,kBAAAA;EAnClD,OAmCkDA;;;EAChD,cAAc;AACZ,UAAM,sCAAmC,KAAKO,0BAA0BE,iBAAiB;EAC3F;AACF;AAIO,IAAMQ,gCAAN,cAA4CjB,kBAAAA;EA3CnD,OA2CmDA;;;EACjD,YAAYkB,OAAe;AACzB,UAAM,iCAAiC,KAAKX,0BAA0BG,oBAAoB;MAAEQ;IAAM,CAAA;EACpG;AACF;AAEO,IAAMC,qBAAN,cAAiCnB,kBAAAA;EAjDxC,OAiDwCA;;;EACtC,YAAYoB,cAAsB;AAChC,UAAM,2CAAqCA,YAAAA,KAAiB,KAAKb,0BAA0BI,gBAAgB;MACzGS;IACF,CAAA;EACF;AACF;AAEO,IAAMC,6BAAN,cAAyCrB,kBAAAA;EAzDhD,OAyDgDA;;;EAC9C,cAAc;AACZ,UACE,sEACA,KACAO,0BAA0BK,uBAAuB;EAErD;AACF;AAEO,IAAMU,uBAAN,cAAmCtB,kBAAAA;EAnE1C,OAmE0CA;;;EACxC,YAAYuB,gBAAwB;AAClC,UACE,gDAA0CA,cAAAA,KAC1C,KACAhB,0BAA0BM,mBAC1B;MAAEU;IAAe,CAAA;EAErB;AACF;","names":["z","whatsAppMediaSchema","object","id","string","mime_type","sha256","optional","caption","filename","whatsAppInteractiveSchema","type","button_reply","title","list_reply","whatsAppOrderSchema","catalog_id","text","product_items","array","product_retailer_id","quantity","number","item_price","currency","whatsAppMessageSchema","from","body","image","audio","video","document","sticker","interactive","order","context","referred_product","timestamp","whatsAppMessageEchoSchema","whatsAppMessageStatusSchema","enum","whatsAppStatusSchema","status","recipient_id","whatsAppWebhookPayloadSchema","entry","changes","value","messaging_product","messages","message_echoes","statuses","metadata","display_phone_number","phone_number_id","z","FLOW_ACTION_KIND","HANDOFF","RATE_LIMITED_HANDOFF","SEND_PRODUCT_LIST","SEND_MEDIA","CROSS_FLOW_PREFIX","isCrossFlowTarget","target","startsWith","crossFlowKey","slice","length","flowNodeNextSchema","z","union","string","object","byAnswer","record","default","flowNodeDataSchema","id","type","enum","contextKey","optional","questionType","question","options","array","tuple","actionKind","actionParams","unknown","directMessage","fallbackMessage","conditionContextKey","conditionOperator","conditionValue","position","x","number","y","next","passthrough","flowGraphNodesSchema","MetaWhatsAppError","Error","message","statusCode","code","details","name","META_WHATSAPP_ERROR_CODES","WINDOW_EXPIRED","INVALID_SIGNATURE","DUPLICATE_DELIVERY","CONFIG_MISSING","TEMPLATE_NOT_CONFIGURED","SESSION_NOT_FOUND","WindowExpiredError","hoursSinceLastMessage","InvalidWebhookSignatureError","DuplicateWebhookDeliveryError","nonce","ConfigMissingError","missingField","TemplateNotConfiguredError","SessionNotFoundError","whatsappNumber"]}
|
package/dist/testing/index.cjs
CHANGED
|
@@ -27,6 +27,7 @@ __export(testing_exports, {
|
|
|
27
27
|
PREVIEW_WABA_ID: () => PREVIEW_WABA_ID,
|
|
28
28
|
buildInboundAudioPayload: () => buildInboundAudioPayload,
|
|
29
29
|
buildInboundInteractivePayload: () => buildInboundInteractivePayload,
|
|
30
|
+
buildInboundMediaPayload: () => buildInboundMediaPayload,
|
|
30
31
|
buildInboundTextPayload: () => buildInboundTextPayload,
|
|
31
32
|
serializeWebhookPayload: () => serializeWebhookPayload
|
|
32
33
|
});
|
|
@@ -125,6 +126,31 @@ function buildInboundAudioPayload(params) {
|
|
|
125
126
|
});
|
|
126
127
|
}
|
|
127
128
|
__name(buildInboundAudioPayload, "buildInboundAudioPayload");
|
|
129
|
+
function buildInboundMediaPayload(params) {
|
|
130
|
+
const { mediaType, mediaId, mimeType, filename, caption, ...envelope } = params;
|
|
131
|
+
return buildEnvelope({
|
|
132
|
+
...envelope,
|
|
133
|
+
message: {
|
|
134
|
+
id: generateWamid(),
|
|
135
|
+
from: params.from,
|
|
136
|
+
type: mediaType,
|
|
137
|
+
[mediaType]: {
|
|
138
|
+
id: mediaId,
|
|
139
|
+
...mimeType ? {
|
|
140
|
+
mime_type: mimeType
|
|
141
|
+
} : {},
|
|
142
|
+
...filename ? {
|
|
143
|
+
filename
|
|
144
|
+
} : {},
|
|
145
|
+
...caption ? {
|
|
146
|
+
caption
|
|
147
|
+
} : {}
|
|
148
|
+
},
|
|
149
|
+
timestamp: currentTimestamp()
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
__name(buildInboundMediaPayload, "buildInboundMediaPayload");
|
|
128
154
|
function serializeWebhookPayload(payload) {
|
|
129
155
|
return JSON.stringify(payload);
|
|
130
156
|
}
|
|
@@ -137,6 +163,7 @@ __name(serializeWebhookPayload, "serializeWebhookPayload");
|
|
|
137
163
|
PREVIEW_WABA_ID,
|
|
138
164
|
buildInboundAudioPayload,
|
|
139
165
|
buildInboundInteractivePayload,
|
|
166
|
+
buildInboundMediaPayload,
|
|
140
167
|
buildInboundTextPayload,
|
|
141
168
|
serializeWebhookPayload
|
|
142
169
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/testing/index.ts","../../src/testing/inboundPayloads.ts"],"sourcesContent":["/**\n * Export separado (`@adatechnology/meta-whatsapp-contracts/testing`): utilitário de\n * desenvolvimento não entra no grafo de import de quem consome os contratos em produção.\n */\n\nexport {\n buildInboundTextPayload,\n buildInboundInteractivePayload,\n buildInboundAudioPayload,\n serializeWebhookPayload,\n PREVIEW_PHONE_NUMBER_ID,\n PREVIEW_WABA_ID,\n PREVIEW_DISPLAY_PHONE_NUMBER,\n PREVIEW_AUDIO_MIME_TYPE,\n} from './inboundPayloads'\n\nexport type {\n BuildInboundTextPayloadParams,\n BuildInboundInteractivePayloadParams,\n BuildInboundAudioPayloadParams,\n InteractiveReplyOption,\n} from './inboundPayloads'\n","/**\n * Builders de payload inbound para exercitar o webhook em desenvolvimento sem um app da Meta\n * credenciado. Moram aqui, e não no módulo, por uma razão de runtime: o preview do navegador\n * precisa montar payloads, e o módulo depende de `node:crypto` — importá-lo do frontend quebraria\n * o bundle. Este pacote só depende de zod, então roda nos dois lados.\n *\n * São construção de payload, não assinatura: assinar depende do runtime (`node:crypto` no\n * servidor, WebCrypto no navegador) e fica com quem tem o segredo.\n *\n * Por que `id` e `timestamp` são novos a cada chamada: o anti-replay do webhook usa o header de\n * assinatura como nonce, e a assinatura é derivada do corpo cru. Dois payloads idênticos geram a\n * mesma assinatura, e a segunda entrega é descartada como duplicata — numa conversa real (\"sim\"\n * duas vezes) isso apareceria como mensagem engolida, sem erro nenhum para investigar.\n */\n\nimport type { WhatsAppMessage, WhatsAppWebhookPayload } from '../webhook.types'\n\n// Valores de dev que espelham o formato dos reais (15 dígitos) sem apontar para nada da Meta.\nexport const PREVIEW_PHONE_NUMBER_ID = '000000000000000'\nexport const PREVIEW_WABA_ID = '000000000000001'\nexport const PREVIEW_DISPLAY_PHONE_NUMBER = '+5511900000000'\n\n// O que a Meta manda para áudio gravado no app; o pipeline de STT depende do mime para decodificar.\nexport const PREVIEW_AUDIO_MIME_TYPE = 'audio/ogg; codecs=opus'\n\ntype InboundEnvelopeParams = {\n readonly from: string\n readonly phoneNumberId?: string\n readonly displayPhoneNumber?: string\n readonly wabaId?: string\n}\n\n// `globalThis.crypto` em vez de `node:crypto`: é o que existe tanto no navegador quanto no Node\n// 19+/Bun, e é o que mantém estes builders utilizáveis pelo preview do navegador.\nfunction generateWamid(): string {\n // Formato real é `wamid.<base64>`. O prefixo importa para quem faz parsing por prefixo; o corpo\n // só precisa ser único por entrega, que é o que mantém a assinatura — e o nonce — distinta.\n const unique = globalThis.crypto.randomUUID().replaceAll('-', '')\n return `wamid.${unique}`\n}\n\nfunction currentTimestamp(): string {\n return Math.floor(Date.now() / 1000).toString()\n}\n\ntype BuildEnvelopeParams = InboundEnvelopeParams & {\n readonly message: WhatsAppMessage\n}\n\nfunction buildEnvelope(params: BuildEnvelopeParams): WhatsAppWebhookPayload {\n return {\n object: 'whatsapp_business_account',\n entry: [\n {\n id: params.wabaId ?? PREVIEW_WABA_ID,\n changes: [\n {\n value: {\n messaging_product: 'whatsapp',\n metadata: {\n display_phone_number: params.displayPhoneNumber ?? PREVIEW_DISPLAY_PHONE_NUMBER,\n phone_number_id: params.phoneNumberId ?? PREVIEW_PHONE_NUMBER_ID,\n },\n messages: [params.message],\n },\n },\n ],\n },\n ],\n }\n}\n\nexport type BuildInboundTextPayloadParams = InboundEnvelopeParams & {\n readonly text: string\n}\n\nexport function buildInboundTextPayload(params: BuildInboundTextPayloadParams): WhatsAppWebhookPayload {\n const { text, ...envelope } = params\n\n return buildEnvelope({\n ...envelope,\n message: {\n id: generateWamid(),\n from: params.from,\n type: 'text',\n text: { body: text },\n timestamp: currentTimestamp(),\n },\n })\n}\n\nexport type InteractiveReplyOption = {\n readonly id: string\n readonly title: string\n}\n\n// União exclusiva: uma resposta interativa é de botão OU de lista, nunca das duas nem de nenhuma.\n// O tipo torna o erro impossível em vez de validá-lo em runtime.\nexport type BuildInboundInteractivePayloadParams = InboundEnvelopeParams &\n (\n | { readonly buttonReply: InteractiveReplyOption; readonly listReply?: never }\n | { readonly listReply: InteractiveReplyOption; readonly buttonReply?: never }\n )\n\nexport function buildInboundInteractivePayload(params: BuildInboundInteractivePayloadParams): WhatsAppWebhookPayload {\n const { buttonReply, listReply, ...envelope } = params\n\n const interactive = buttonReply\n ? { type: 'button_reply', button_reply: buttonReply }\n : { type: 'list_reply', list_reply: listReply }\n\n return buildEnvelope({\n ...envelope,\n message: {\n id: generateWamid(),\n from: params.from,\n type: 'interactive',\n interactive,\n timestamp: currentTimestamp(),\n },\n })\n}\n\nexport type BuildInboundAudioPayloadParams = InboundEnvelopeParams & {\n readonly mediaId: string\n readonly mimeType?: string\n}\n\nexport function buildInboundAudioPayload(params: BuildInboundAudioPayloadParams): WhatsAppWebhookPayload {\n const { mediaId, mimeType, ...envelope } = params\n\n return buildEnvelope({\n ...envelope,\n message: {\n id: generateWamid(),\n from: params.from,\n type: 'audio',\n audio: { id: mediaId, mime_type: mimeType ?? PREVIEW_AUDIO_MIME_TYPE },\n timestamp: currentTimestamp(),\n },\n })\n}\n\n/**\n * Serializa o payload uma única vez. A validação assina os bytes exatos recebidos: quem reserializa\n * antes de enviar (ou deixa o cliente HTTP serializar o objeto) muda espaçamento/ordem e derruba a\n * assinatura. Assinar e enviar SEMPRE a string devolvida aqui é o contrato.\n */\nexport function serializeWebhookPayload(payload: WhatsAppWebhookPayload): string {\n return JSON.stringify(payload)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAAA;;;;;;;;;;;;;;ACkBO,IAAMA,0BAA0B;AAChC,IAAMC,kBAAkB;AACxB,IAAMC,+BAA+B;AAGrC,IAAMC,0BAA0B;AAWvC,SAASC,gBAAAA;AAGP,QAAMC,SAASC,WAAWC,OAAOC,WAAU,EAAGC,WAAW,KAAK,EAAA;AAC9D,SAAO,SAASJ,MAAAA;AAClB;AALSD;AAOT,SAASM,mBAAAA;AACP,SAAOC,KAAKC,MAAMC,KAAKC,IAAG,IAAK,GAAA,EAAMC,SAAQ;AAC/C;AAFSL;AAQT,SAASM,cAAcC,QAA2B;AAChD,SAAO;IACLC,QAAQ;IACRC,OAAO;MACL;QACEC,IAAIH,OAAOI,UAAUpB;QACrBqB,SAAS;UACP;YACEC,OAAO;cACLC,mBAAmB;cACnBC,UAAU;gBACRC,sBAAsBT,OAAOU,sBAAsBzB;gBACnD0B,iBAAiBX,OAAOY,iBAAiB7B;cAC3C;cACA8B,UAAU;gBAACb,OAAOc;;YACpB;UACF;;MAEJ;;EAEJ;AACF;AArBSf;AA2BF,SAASgB,wBAAwBf,QAAqC;AAC3E,QAAM,EAAEgB,MAAM,GAAGC,SAAAA,IAAajB;AAE9B,SAAOD,cAAc;IACnB,GAAGkB;IACHH,SAAS;MACPX,IAAIhB,cAAAA;MACJ+B,MAAMlB,OAAOkB;MACbC,MAAM;MACNH,MAAM;QAAEI,MAAMJ;MAAK;MACnBK,WAAW5B,iBAAAA;IACb;EACF,CAAA;AACF;AAbgBsB;AA4BT,SAASO,+BAA+BtB,QAA4C;AACzF,QAAM,EAAEuB,aAAaC,WAAW,GAAGP,SAAAA,IAAajB;AAEhD,QAAMyB,cAAcF,cAChB;IAAEJ,MAAM;IAAgBO,cAAcH;EAAY,IAClD;IAAEJ,MAAM;IAAcQ,YAAYH;EAAU;AAEhD,SAAOzB,cAAc;IACnB,GAAGkB;IACHH,SAAS;MACPX,IAAIhB,cAAAA;MACJ+B,MAAMlB,OAAOkB;MACbC,MAAM;MACNM;MACAJ,WAAW5B,iBAAAA;IACb;EACF,CAAA;AACF;AAjBgB6B;AAwBT,SAASM,yBAAyB5B,QAAsC;AAC7E,QAAM,EAAE6B,SAASC,UAAU,GAAGb,SAAAA,IAAajB;AAE3C,SAAOD,cAAc;IACnB,GAAGkB;IACHH,SAAS;MACPX,IAAIhB,cAAAA;MACJ+B,MAAMlB,OAAOkB;MACbC,MAAM;MACNY,OAAO;QAAE5B,IAAI0B;QAASG,WAAWF,YAAY5C;MAAwB;MACrEmC,WAAW5B,iBAAAA;IACb;EACF,CAAA;AACF;AAbgBmC;AAoBT,SAASK,wBAAwBC,SAA+B;AACrE,SAAOC,KAAKC,UAAUF,OAAAA;AACxB;AAFgBD;","names":["PREVIEW_PHONE_NUMBER_ID","PREVIEW_WABA_ID","PREVIEW_DISPLAY_PHONE_NUMBER","PREVIEW_AUDIO_MIME_TYPE","generateWamid","unique","globalThis","crypto","randomUUID","replaceAll","currentTimestamp","Math","floor","Date","now","toString","buildEnvelope","params","object","entry","id","wabaId","changes","value","messaging_product","metadata","display_phone_number","displayPhoneNumber","phone_number_id","phoneNumberId","messages","message","buildInboundTextPayload","text","envelope","from","type","body","timestamp","buildInboundInteractivePayload","buttonReply","listReply","interactive","button_reply","list_reply","buildInboundAudioPayload","mediaId","mimeType","audio","mime_type","serializeWebhookPayload","payload","JSON","stringify"]}
|
|
1
|
+
{"version":3,"sources":["../../src/testing/index.ts","../../src/testing/inboundPayloads.ts"],"sourcesContent":["/**\n * Export separado (`@adatechnology/meta-whatsapp-contracts/testing`): utilitário de\n * desenvolvimento não entra no grafo de import de quem consome os contratos em produção.\n */\n\nexport {\n buildInboundTextPayload,\n buildInboundInteractivePayload,\n buildInboundAudioPayload,\n buildInboundMediaPayload,\n serializeWebhookPayload,\n PREVIEW_PHONE_NUMBER_ID,\n PREVIEW_WABA_ID,\n PREVIEW_DISPLAY_PHONE_NUMBER,\n PREVIEW_AUDIO_MIME_TYPE,\n} from './inboundPayloads'\n\nexport type {\n BuildInboundTextPayloadParams,\n BuildInboundInteractivePayloadParams,\n BuildInboundAudioPayloadParams,\n BuildInboundMediaPayloadParams,\n InboundMediaType,\n InteractiveReplyOption,\n} from './inboundPayloads'\n","/**\n * Builders de payload inbound para exercitar o webhook em desenvolvimento sem um app da Meta\n * credenciado. Moram aqui, e não no módulo, por uma razão de runtime: o preview do navegador\n * precisa montar payloads, e o módulo depende de `node:crypto` — importá-lo do frontend quebraria\n * o bundle. Este pacote só depende de zod, então roda nos dois lados.\n *\n * São construção de payload, não assinatura: assinar depende do runtime (`node:crypto` no\n * servidor, WebCrypto no navegador) e fica com quem tem o segredo.\n *\n * Por que `id` e `timestamp` são novos a cada chamada: o anti-replay do webhook usa o header de\n * assinatura como nonce, e a assinatura é derivada do corpo cru. Dois payloads idênticos geram a\n * mesma assinatura, e a segunda entrega é descartada como duplicata — numa conversa real (\"sim\"\n * duas vezes) isso apareceria como mensagem engolida, sem erro nenhum para investigar.\n */\n\nimport type { WhatsAppMessage, WhatsAppWebhookPayload } from '../webhook.types'\n\n// Valores de dev que espelham o formato dos reais (15 dígitos) sem apontar para nada da Meta.\nexport const PREVIEW_PHONE_NUMBER_ID = '000000000000000'\nexport const PREVIEW_WABA_ID = '000000000000001'\nexport const PREVIEW_DISPLAY_PHONE_NUMBER = '+5511900000000'\n\n// O que a Meta manda para áudio gravado no app; o pipeline de STT depende do mime para decodificar.\nexport const PREVIEW_AUDIO_MIME_TYPE = 'audio/ogg; codecs=opus'\n\ntype InboundEnvelopeParams = {\n readonly from: string\n readonly phoneNumberId?: string\n readonly displayPhoneNumber?: string\n readonly wabaId?: string\n}\n\n// `globalThis.crypto` em vez de `node:crypto`: é o que existe tanto no navegador quanto no Node\n// 19+/Bun, e é o que mantém estes builders utilizáveis pelo preview do navegador.\nfunction generateWamid(): string {\n // Formato real é `wamid.<base64>`. O prefixo importa para quem faz parsing por prefixo; o corpo\n // só precisa ser único por entrega, que é o que mantém a assinatura — e o nonce — distinta.\n const unique = globalThis.crypto.randomUUID().replaceAll('-', '')\n return `wamid.${unique}`\n}\n\nfunction currentTimestamp(): string {\n return Math.floor(Date.now() / 1000).toString()\n}\n\ntype BuildEnvelopeParams = InboundEnvelopeParams & {\n readonly message: WhatsAppMessage\n}\n\nfunction buildEnvelope(params: BuildEnvelopeParams): WhatsAppWebhookPayload {\n return {\n object: 'whatsapp_business_account',\n entry: [\n {\n id: params.wabaId ?? PREVIEW_WABA_ID,\n changes: [\n {\n value: {\n messaging_product: 'whatsapp',\n metadata: {\n display_phone_number: params.displayPhoneNumber ?? PREVIEW_DISPLAY_PHONE_NUMBER,\n phone_number_id: params.phoneNumberId ?? PREVIEW_PHONE_NUMBER_ID,\n },\n messages: [params.message],\n },\n },\n ],\n },\n ],\n }\n}\n\nexport type BuildInboundTextPayloadParams = InboundEnvelopeParams & {\n readonly text: string\n}\n\nexport function buildInboundTextPayload(params: BuildInboundTextPayloadParams): WhatsAppWebhookPayload {\n const { text, ...envelope } = params\n\n return buildEnvelope({\n ...envelope,\n message: {\n id: generateWamid(),\n from: params.from,\n type: 'text',\n text: { body: text },\n timestamp: currentTimestamp(),\n },\n })\n}\n\nexport type InteractiveReplyOption = {\n readonly id: string\n readonly title: string\n}\n\n// União exclusiva: uma resposta interativa é de botão OU de lista, nunca das duas nem de nenhuma.\n// O tipo torna o erro impossível em vez de validá-lo em runtime.\nexport type BuildInboundInteractivePayloadParams = InboundEnvelopeParams &\n (\n | { readonly buttonReply: InteractiveReplyOption; readonly listReply?: never }\n | { readonly listReply: InteractiveReplyOption; readonly buttonReply?: never }\n )\n\nexport function buildInboundInteractivePayload(params: BuildInboundInteractivePayloadParams): WhatsAppWebhookPayload {\n const { buttonReply, listReply, ...envelope } = params\n\n const interactive = buttonReply\n ? { type: 'button_reply', button_reply: buttonReply }\n : { type: 'list_reply', list_reply: listReply }\n\n return buildEnvelope({\n ...envelope,\n message: {\n id: generateWamid(),\n from: params.from,\n type: 'interactive',\n interactive,\n timestamp: currentTimestamp(),\n },\n })\n}\n\nexport type BuildInboundAudioPayloadParams = InboundEnvelopeParams & {\n readonly mediaId: string\n readonly mimeType?: string\n}\n\nexport function buildInboundAudioPayload(params: BuildInboundAudioPayloadParams): WhatsAppWebhookPayload {\n const { mediaId, mimeType, ...envelope } = params\n\n return buildEnvelope({\n ...envelope,\n message: {\n id: generateWamid(),\n from: params.from,\n type: 'audio',\n audio: { id: mediaId, mime_type: mimeType ?? PREVIEW_AUDIO_MIME_TYPE },\n timestamp: currentTimestamp(),\n },\n })\n}\n\n/** Tipos de mídia que a Meta entrega por `id`, para o consumidor baixar depois. */\nexport type InboundMediaType = 'image' | 'video' | 'audio' | 'document' | 'sticker'\n\nexport type BuildInboundMediaPayloadParams = InboundEnvelopeParams & {\n readonly mediaType: InboundMediaType\n readonly mediaId: string\n readonly mimeType?: string\n readonly filename?: string\n readonly caption?: string\n}\n\n/**\n * Mídia genérica. O bloco vai com o nome do próprio tipo (`image`, `document`, …) porque é assim que\n * a Meta monta o webhook — e é por esse nome que o consumidor localiza o `id` para baixar o arquivo.\n *\n * `filename` só existe em documento e `caption` só em imagem/vídeo/documento; enviar os dois em\n * áudio geraria um payload que a Meta nunca produz, então eles são omitidos quando vazios.\n */\nexport function buildInboundMediaPayload(params: BuildInboundMediaPayloadParams): WhatsAppWebhookPayload {\n const { mediaType, mediaId, mimeType, filename, caption, ...envelope } = params\n\n return buildEnvelope({\n ...envelope,\n message: {\n id: generateWamid(),\n from: params.from,\n type: mediaType,\n [mediaType]: {\n id: mediaId,\n ...(mimeType ? { mime_type: mimeType } : {}),\n ...(filename ? { filename } : {}),\n ...(caption ? { caption } : {}),\n },\n timestamp: currentTimestamp(),\n },\n })\n}\n\n/**\n * Serializa o payload uma única vez. A validação assina os bytes exatos recebidos: quem reserializa\n * antes de enviar (ou deixa o cliente HTTP serializar o objeto) muda espaçamento/ordem e derruba a\n * assinatura. Assinar e enviar SEMPRE a string devolvida aqui é o contrato.\n */\nexport function serializeWebhookPayload(payload: WhatsAppWebhookPayload): string {\n return JSON.stringify(payload)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAAA;;;;;;;;;;;;;;;ACkBO,IAAMA,0BAA0B;AAChC,IAAMC,kBAAkB;AACxB,IAAMC,+BAA+B;AAGrC,IAAMC,0BAA0B;AAWvC,SAASC,gBAAAA;AAGP,QAAMC,SAASC,WAAWC,OAAOC,WAAU,EAAGC,WAAW,KAAK,EAAA;AAC9D,SAAO,SAASJ,MAAAA;AAClB;AALSD;AAOT,SAASM,mBAAAA;AACP,SAAOC,KAAKC,MAAMC,KAAKC,IAAG,IAAK,GAAA,EAAMC,SAAQ;AAC/C;AAFSL;AAQT,SAASM,cAAcC,QAA2B;AAChD,SAAO;IACLC,QAAQ;IACRC,OAAO;MACL;QACEC,IAAIH,OAAOI,UAAUpB;QACrBqB,SAAS;UACP;YACEC,OAAO;cACLC,mBAAmB;cACnBC,UAAU;gBACRC,sBAAsBT,OAAOU,sBAAsBzB;gBACnD0B,iBAAiBX,OAAOY,iBAAiB7B;cAC3C;cACA8B,UAAU;gBAACb,OAAOc;;YACpB;UACF;;MAEJ;;EAEJ;AACF;AArBSf;AA2BF,SAASgB,wBAAwBf,QAAqC;AAC3E,QAAM,EAAEgB,MAAM,GAAGC,SAAAA,IAAajB;AAE9B,SAAOD,cAAc;IACnB,GAAGkB;IACHH,SAAS;MACPX,IAAIhB,cAAAA;MACJ+B,MAAMlB,OAAOkB;MACbC,MAAM;MACNH,MAAM;QAAEI,MAAMJ;MAAK;MACnBK,WAAW5B,iBAAAA;IACb;EACF,CAAA;AACF;AAbgBsB;AA4BT,SAASO,+BAA+BtB,QAA4C;AACzF,QAAM,EAAEuB,aAAaC,WAAW,GAAGP,SAAAA,IAAajB;AAEhD,QAAMyB,cAAcF,cAChB;IAAEJ,MAAM;IAAgBO,cAAcH;EAAY,IAClD;IAAEJ,MAAM;IAAcQ,YAAYH;EAAU;AAEhD,SAAOzB,cAAc;IACnB,GAAGkB;IACHH,SAAS;MACPX,IAAIhB,cAAAA;MACJ+B,MAAMlB,OAAOkB;MACbC,MAAM;MACNM;MACAJ,WAAW5B,iBAAAA;IACb;EACF,CAAA;AACF;AAjBgB6B;AAwBT,SAASM,yBAAyB5B,QAAsC;AAC7E,QAAM,EAAE6B,SAASC,UAAU,GAAGb,SAAAA,IAAajB;AAE3C,SAAOD,cAAc;IACnB,GAAGkB;IACHH,SAAS;MACPX,IAAIhB,cAAAA;MACJ+B,MAAMlB,OAAOkB;MACbC,MAAM;MACNY,OAAO;QAAE5B,IAAI0B;QAASG,WAAWF,YAAY5C;MAAwB;MACrEmC,WAAW5B,iBAAAA;IACb;EACF,CAAA;AACF;AAbgBmC;AAiCT,SAASK,yBAAyBjC,QAAsC;AAC7E,QAAM,EAAEkC,WAAWL,SAASC,UAAUK,UAAUC,SAAS,GAAGnB,SAAAA,IAAajB;AAEzE,SAAOD,cAAc;IACnB,GAAGkB;IACHH,SAAS;MACPX,IAAIhB,cAAAA;MACJ+B,MAAMlB,OAAOkB;MACbC,MAAMe;MACN,CAACA,SAAAA,GAAY;QACX/B,IAAI0B;QACJ,GAAIC,WAAW;UAAEE,WAAWF;QAAS,IAAI,CAAC;QAC1C,GAAIK,WAAW;UAAEA;QAAS,IAAI,CAAC;QAC/B,GAAIC,UAAU;UAAEA;QAAQ,IAAI,CAAC;MAC/B;MACAf,WAAW5B,iBAAAA;IACb;EACF,CAAA;AACF;AAlBgBwC;AAyBT,SAASI,wBAAwBC,SAA+B;AACrE,SAAOC,KAAKC,UAAUF,OAAAA;AACxB;AAFgBD;","names":["PREVIEW_PHONE_NUMBER_ID","PREVIEW_WABA_ID","PREVIEW_DISPLAY_PHONE_NUMBER","PREVIEW_AUDIO_MIME_TYPE","generateWamid","unique","globalThis","crypto","randomUUID","replaceAll","currentTimestamp","Math","floor","Date","now","toString","buildEnvelope","params","object","entry","id","wabaId","changes","value","messaging_product","metadata","display_phone_number","displayPhoneNumber","phone_number_id","phoneNumberId","messages","message","buildInboundTextPayload","text","envelope","from","type","body","timestamp","buildInboundInteractivePayload","buttonReply","listReply","interactive","button_reply","list_reply","buildInboundAudioPayload","mediaId","mimeType","audio","mime_type","buildInboundMediaPayload","mediaType","filename","caption","serializeWebhookPayload","payload","JSON","stringify"]}
|
package/dist/testing/index.d.cts
CHANGED
|
@@ -47,6 +47,23 @@ type BuildInboundAudioPayloadParams = InboundEnvelopeParams & {
|
|
|
47
47
|
readonly mimeType?: string;
|
|
48
48
|
};
|
|
49
49
|
declare function buildInboundAudioPayload(params: BuildInboundAudioPayloadParams): WhatsAppWebhookPayload;
|
|
50
|
+
/** Tipos de mídia que a Meta entrega por `id`, para o consumidor baixar depois. */
|
|
51
|
+
type InboundMediaType = 'image' | 'video' | 'audio' | 'document' | 'sticker';
|
|
52
|
+
type BuildInboundMediaPayloadParams = InboundEnvelopeParams & {
|
|
53
|
+
readonly mediaType: InboundMediaType;
|
|
54
|
+
readonly mediaId: string;
|
|
55
|
+
readonly mimeType?: string;
|
|
56
|
+
readonly filename?: string;
|
|
57
|
+
readonly caption?: string;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Mídia genérica. O bloco vai com o nome do próprio tipo (`image`, `document`, …) porque é assim que
|
|
61
|
+
* a Meta monta o webhook — e é por esse nome que o consumidor localiza o `id` para baixar o arquivo.
|
|
62
|
+
*
|
|
63
|
+
* `filename` só existe em documento e `caption` só em imagem/vídeo/documento; enviar os dois em
|
|
64
|
+
* áudio geraria um payload que a Meta nunca produz, então eles são omitidos quando vazios.
|
|
65
|
+
*/
|
|
66
|
+
declare function buildInboundMediaPayload(params: BuildInboundMediaPayloadParams): WhatsAppWebhookPayload;
|
|
50
67
|
/**
|
|
51
68
|
* Serializa o payload uma única vez. A validação assina os bytes exatos recebidos: quem reserializa
|
|
52
69
|
* antes de enviar (ou deixa o cliente HTTP serializar o objeto) muda espaçamento/ordem e derruba a
|
|
@@ -54,4 +71,4 @@ declare function buildInboundAudioPayload(params: BuildInboundAudioPayloadParams
|
|
|
54
71
|
*/
|
|
55
72
|
declare function serializeWebhookPayload(payload: WhatsAppWebhookPayload): string;
|
|
56
73
|
|
|
57
|
-
export { type BuildInboundAudioPayloadParams, type BuildInboundInteractivePayloadParams, type BuildInboundTextPayloadParams, type InteractiveReplyOption, PREVIEW_AUDIO_MIME_TYPE, PREVIEW_DISPLAY_PHONE_NUMBER, PREVIEW_PHONE_NUMBER_ID, PREVIEW_WABA_ID, buildInboundAudioPayload, buildInboundInteractivePayload, buildInboundTextPayload, serializeWebhookPayload };
|
|
74
|
+
export { type BuildInboundAudioPayloadParams, type BuildInboundInteractivePayloadParams, type BuildInboundMediaPayloadParams, type BuildInboundTextPayloadParams, type InboundMediaType, type InteractiveReplyOption, PREVIEW_AUDIO_MIME_TYPE, PREVIEW_DISPLAY_PHONE_NUMBER, PREVIEW_PHONE_NUMBER_ID, PREVIEW_WABA_ID, buildInboundAudioPayload, buildInboundInteractivePayload, buildInboundMediaPayload, buildInboundTextPayload, serializeWebhookPayload };
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -47,6 +47,23 @@ type BuildInboundAudioPayloadParams = InboundEnvelopeParams & {
|
|
|
47
47
|
readonly mimeType?: string;
|
|
48
48
|
};
|
|
49
49
|
declare function buildInboundAudioPayload(params: BuildInboundAudioPayloadParams): WhatsAppWebhookPayload;
|
|
50
|
+
/** Tipos de mídia que a Meta entrega por `id`, para o consumidor baixar depois. */
|
|
51
|
+
type InboundMediaType = 'image' | 'video' | 'audio' | 'document' | 'sticker';
|
|
52
|
+
type BuildInboundMediaPayloadParams = InboundEnvelopeParams & {
|
|
53
|
+
readonly mediaType: InboundMediaType;
|
|
54
|
+
readonly mediaId: string;
|
|
55
|
+
readonly mimeType?: string;
|
|
56
|
+
readonly filename?: string;
|
|
57
|
+
readonly caption?: string;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Mídia genérica. O bloco vai com o nome do próprio tipo (`image`, `document`, …) porque é assim que
|
|
61
|
+
* a Meta monta o webhook — e é por esse nome que o consumidor localiza o `id` para baixar o arquivo.
|
|
62
|
+
*
|
|
63
|
+
* `filename` só existe em documento e `caption` só em imagem/vídeo/documento; enviar os dois em
|
|
64
|
+
* áudio geraria um payload que a Meta nunca produz, então eles são omitidos quando vazios.
|
|
65
|
+
*/
|
|
66
|
+
declare function buildInboundMediaPayload(params: BuildInboundMediaPayloadParams): WhatsAppWebhookPayload;
|
|
50
67
|
/**
|
|
51
68
|
* Serializa o payload uma única vez. A validação assina os bytes exatos recebidos: quem reserializa
|
|
52
69
|
* antes de enviar (ou deixa o cliente HTTP serializar o objeto) muda espaçamento/ordem e derruba a
|
|
@@ -54,4 +71,4 @@ declare function buildInboundAudioPayload(params: BuildInboundAudioPayloadParams
|
|
|
54
71
|
*/
|
|
55
72
|
declare function serializeWebhookPayload(payload: WhatsAppWebhookPayload): string;
|
|
56
73
|
|
|
57
|
-
export { type BuildInboundAudioPayloadParams, type BuildInboundInteractivePayloadParams, type BuildInboundTextPayloadParams, type InteractiveReplyOption, PREVIEW_AUDIO_MIME_TYPE, PREVIEW_DISPLAY_PHONE_NUMBER, PREVIEW_PHONE_NUMBER_ID, PREVIEW_WABA_ID, buildInboundAudioPayload, buildInboundInteractivePayload, buildInboundTextPayload, serializeWebhookPayload };
|
|
74
|
+
export { type BuildInboundAudioPayloadParams, type BuildInboundInteractivePayloadParams, type BuildInboundMediaPayloadParams, type BuildInboundTextPayloadParams, type InboundMediaType, type InteractiveReplyOption, PREVIEW_AUDIO_MIME_TYPE, PREVIEW_DISPLAY_PHONE_NUMBER, PREVIEW_PHONE_NUMBER_ID, PREVIEW_WABA_ID, buildInboundAudioPayload, buildInboundInteractivePayload, buildInboundMediaPayload, buildInboundTextPayload, serializeWebhookPayload };
|
package/dist/testing/index.js
CHANGED
|
@@ -94,6 +94,31 @@ function buildInboundAudioPayload(params) {
|
|
|
94
94
|
});
|
|
95
95
|
}
|
|
96
96
|
__name(buildInboundAudioPayload, "buildInboundAudioPayload");
|
|
97
|
+
function buildInboundMediaPayload(params) {
|
|
98
|
+
const { mediaType, mediaId, mimeType, filename, caption, ...envelope } = params;
|
|
99
|
+
return buildEnvelope({
|
|
100
|
+
...envelope,
|
|
101
|
+
message: {
|
|
102
|
+
id: generateWamid(),
|
|
103
|
+
from: params.from,
|
|
104
|
+
type: mediaType,
|
|
105
|
+
[mediaType]: {
|
|
106
|
+
id: mediaId,
|
|
107
|
+
...mimeType ? {
|
|
108
|
+
mime_type: mimeType
|
|
109
|
+
} : {},
|
|
110
|
+
...filename ? {
|
|
111
|
+
filename
|
|
112
|
+
} : {},
|
|
113
|
+
...caption ? {
|
|
114
|
+
caption
|
|
115
|
+
} : {}
|
|
116
|
+
},
|
|
117
|
+
timestamp: currentTimestamp()
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
__name(buildInboundMediaPayload, "buildInboundMediaPayload");
|
|
97
122
|
function serializeWebhookPayload(payload) {
|
|
98
123
|
return JSON.stringify(payload);
|
|
99
124
|
}
|
|
@@ -105,6 +130,7 @@ export {
|
|
|
105
130
|
PREVIEW_WABA_ID,
|
|
106
131
|
buildInboundAudioPayload,
|
|
107
132
|
buildInboundInteractivePayload,
|
|
133
|
+
buildInboundMediaPayload,
|
|
108
134
|
buildInboundTextPayload,
|
|
109
135
|
serializeWebhookPayload
|
|
110
136
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/testing/inboundPayloads.ts"],"sourcesContent":["/**\n * Builders de payload inbound para exercitar o webhook em desenvolvimento sem um app da Meta\n * credenciado. Moram aqui, e não no módulo, por uma razão de runtime: o preview do navegador\n * precisa montar payloads, e o módulo depende de `node:crypto` — importá-lo do frontend quebraria\n * o bundle. Este pacote só depende de zod, então roda nos dois lados.\n *\n * São construção de payload, não assinatura: assinar depende do runtime (`node:crypto` no\n * servidor, WebCrypto no navegador) e fica com quem tem o segredo.\n *\n * Por que `id` e `timestamp` são novos a cada chamada: o anti-replay do webhook usa o header de\n * assinatura como nonce, e a assinatura é derivada do corpo cru. Dois payloads idênticos geram a\n * mesma assinatura, e a segunda entrega é descartada como duplicata — numa conversa real (\"sim\"\n * duas vezes) isso apareceria como mensagem engolida, sem erro nenhum para investigar.\n */\n\nimport type { WhatsAppMessage, WhatsAppWebhookPayload } from '../webhook.types'\n\n// Valores de dev que espelham o formato dos reais (15 dígitos) sem apontar para nada da Meta.\nexport const PREVIEW_PHONE_NUMBER_ID = '000000000000000'\nexport const PREVIEW_WABA_ID = '000000000000001'\nexport const PREVIEW_DISPLAY_PHONE_NUMBER = '+5511900000000'\n\n// O que a Meta manda para áudio gravado no app; o pipeline de STT depende do mime para decodificar.\nexport const PREVIEW_AUDIO_MIME_TYPE = 'audio/ogg; codecs=opus'\n\ntype InboundEnvelopeParams = {\n readonly from: string\n readonly phoneNumberId?: string\n readonly displayPhoneNumber?: string\n readonly wabaId?: string\n}\n\n// `globalThis.crypto` em vez de `node:crypto`: é o que existe tanto no navegador quanto no Node\n// 19+/Bun, e é o que mantém estes builders utilizáveis pelo preview do navegador.\nfunction generateWamid(): string {\n // Formato real é `wamid.<base64>`. O prefixo importa para quem faz parsing por prefixo; o corpo\n // só precisa ser único por entrega, que é o que mantém a assinatura — e o nonce — distinta.\n const unique = globalThis.crypto.randomUUID().replaceAll('-', '')\n return `wamid.${unique}`\n}\n\nfunction currentTimestamp(): string {\n return Math.floor(Date.now() / 1000).toString()\n}\n\ntype BuildEnvelopeParams = InboundEnvelopeParams & {\n readonly message: WhatsAppMessage\n}\n\nfunction buildEnvelope(params: BuildEnvelopeParams): WhatsAppWebhookPayload {\n return {\n object: 'whatsapp_business_account',\n entry: [\n {\n id: params.wabaId ?? PREVIEW_WABA_ID,\n changes: [\n {\n value: {\n messaging_product: 'whatsapp',\n metadata: {\n display_phone_number: params.displayPhoneNumber ?? PREVIEW_DISPLAY_PHONE_NUMBER,\n phone_number_id: params.phoneNumberId ?? PREVIEW_PHONE_NUMBER_ID,\n },\n messages: [params.message],\n },\n },\n ],\n },\n ],\n }\n}\n\nexport type BuildInboundTextPayloadParams = InboundEnvelopeParams & {\n readonly text: string\n}\n\nexport function buildInboundTextPayload(params: BuildInboundTextPayloadParams): WhatsAppWebhookPayload {\n const { text, ...envelope } = params\n\n return buildEnvelope({\n ...envelope,\n message: {\n id: generateWamid(),\n from: params.from,\n type: 'text',\n text: { body: text },\n timestamp: currentTimestamp(),\n },\n })\n}\n\nexport type InteractiveReplyOption = {\n readonly id: string\n readonly title: string\n}\n\n// União exclusiva: uma resposta interativa é de botão OU de lista, nunca das duas nem de nenhuma.\n// O tipo torna o erro impossível em vez de validá-lo em runtime.\nexport type BuildInboundInteractivePayloadParams = InboundEnvelopeParams &\n (\n | { readonly buttonReply: InteractiveReplyOption; readonly listReply?: never }\n | { readonly listReply: InteractiveReplyOption; readonly buttonReply?: never }\n )\n\nexport function buildInboundInteractivePayload(params: BuildInboundInteractivePayloadParams): WhatsAppWebhookPayload {\n const { buttonReply, listReply, ...envelope } = params\n\n const interactive = buttonReply\n ? { type: 'button_reply', button_reply: buttonReply }\n : { type: 'list_reply', list_reply: listReply }\n\n return buildEnvelope({\n ...envelope,\n message: {\n id: generateWamid(),\n from: params.from,\n type: 'interactive',\n interactive,\n timestamp: currentTimestamp(),\n },\n })\n}\n\nexport type BuildInboundAudioPayloadParams = InboundEnvelopeParams & {\n readonly mediaId: string\n readonly mimeType?: string\n}\n\nexport function buildInboundAudioPayload(params: BuildInboundAudioPayloadParams): WhatsAppWebhookPayload {\n const { mediaId, mimeType, ...envelope } = params\n\n return buildEnvelope({\n ...envelope,\n message: {\n id: generateWamid(),\n from: params.from,\n type: 'audio',\n audio: { id: mediaId, mime_type: mimeType ?? PREVIEW_AUDIO_MIME_TYPE },\n timestamp: currentTimestamp(),\n },\n })\n}\n\n/**\n * Serializa o payload uma única vez. A validação assina os bytes exatos recebidos: quem reserializa\n * antes de enviar (ou deixa o cliente HTTP serializar o objeto) muda espaçamento/ordem e derruba a\n * assinatura. Assinar e enviar SEMPRE a string devolvida aqui é o contrato.\n */\nexport function serializeWebhookPayload(payload: WhatsAppWebhookPayload): string {\n return JSON.stringify(payload)\n}\n"],"mappings":";;;;AAkBO,IAAMA,0BAA0B;AAChC,IAAMC,kBAAkB;AACxB,IAAMC,+BAA+B;AAGrC,IAAMC,0BAA0B;AAWvC,SAASC,gBAAAA;AAGP,QAAMC,SAASC,WAAWC,OAAOC,WAAU,EAAGC,WAAW,KAAK,EAAA;AAC9D,SAAO,SAASJ,MAAAA;AAClB;AALSD;AAOT,SAASM,mBAAAA;AACP,SAAOC,KAAKC,MAAMC,KAAKC,IAAG,IAAK,GAAA,EAAMC,SAAQ;AAC/C;AAFSL;AAQT,SAASM,cAAcC,QAA2B;AAChD,SAAO;IACLC,QAAQ;IACRC,OAAO;MACL;QACEC,IAAIH,OAAOI,UAAUpB;QACrBqB,SAAS;UACP;YACEC,OAAO;cACLC,mBAAmB;cACnBC,UAAU;gBACRC,sBAAsBT,OAAOU,sBAAsBzB;gBACnD0B,iBAAiBX,OAAOY,iBAAiB7B;cAC3C;cACA8B,UAAU;gBAACb,OAAOc;;YACpB;UACF;;MAEJ;;EAEJ;AACF;AArBSf;AA2BF,SAASgB,wBAAwBf,QAAqC;AAC3E,QAAM,EAAEgB,MAAM,GAAGC,SAAAA,IAAajB;AAE9B,SAAOD,cAAc;IACnB,GAAGkB;IACHH,SAAS;MACPX,IAAIhB,cAAAA;MACJ+B,MAAMlB,OAAOkB;MACbC,MAAM;MACNH,MAAM;QAAEI,MAAMJ;MAAK;MACnBK,WAAW5B,iBAAAA;IACb;EACF,CAAA;AACF;AAbgBsB;AA4BT,SAASO,+BAA+BtB,QAA4C;AACzF,QAAM,EAAEuB,aAAaC,WAAW,GAAGP,SAAAA,IAAajB;AAEhD,QAAMyB,cAAcF,cAChB;IAAEJ,MAAM;IAAgBO,cAAcH;EAAY,IAClD;IAAEJ,MAAM;IAAcQ,YAAYH;EAAU;AAEhD,SAAOzB,cAAc;IACnB,GAAGkB;IACHH,SAAS;MACPX,IAAIhB,cAAAA;MACJ+B,MAAMlB,OAAOkB;MACbC,MAAM;MACNM;MACAJ,WAAW5B,iBAAAA;IACb;EACF,CAAA;AACF;AAjBgB6B;AAwBT,SAASM,yBAAyB5B,QAAsC;AAC7E,QAAM,EAAE6B,SAASC,UAAU,GAAGb,SAAAA,IAAajB;AAE3C,SAAOD,cAAc;IACnB,GAAGkB;IACHH,SAAS;MACPX,IAAIhB,cAAAA;MACJ+B,MAAMlB,OAAOkB;MACbC,MAAM;MACNY,OAAO;QAAE5B,IAAI0B;QAASG,WAAWF,YAAY5C;MAAwB;MACrEmC,WAAW5B,iBAAAA;IACb;EACF,CAAA;AACF;AAbgBmC;
|
|
1
|
+
{"version":3,"sources":["../../src/testing/inboundPayloads.ts"],"sourcesContent":["/**\n * Builders de payload inbound para exercitar o webhook em desenvolvimento sem um app da Meta\n * credenciado. Moram aqui, e não no módulo, por uma razão de runtime: o preview do navegador\n * precisa montar payloads, e o módulo depende de `node:crypto` — importá-lo do frontend quebraria\n * o bundle. Este pacote só depende de zod, então roda nos dois lados.\n *\n * São construção de payload, não assinatura: assinar depende do runtime (`node:crypto` no\n * servidor, WebCrypto no navegador) e fica com quem tem o segredo.\n *\n * Por que `id` e `timestamp` são novos a cada chamada: o anti-replay do webhook usa o header de\n * assinatura como nonce, e a assinatura é derivada do corpo cru. Dois payloads idênticos geram a\n * mesma assinatura, e a segunda entrega é descartada como duplicata — numa conversa real (\"sim\"\n * duas vezes) isso apareceria como mensagem engolida, sem erro nenhum para investigar.\n */\n\nimport type { WhatsAppMessage, WhatsAppWebhookPayload } from '../webhook.types'\n\n// Valores de dev que espelham o formato dos reais (15 dígitos) sem apontar para nada da Meta.\nexport const PREVIEW_PHONE_NUMBER_ID = '000000000000000'\nexport const PREVIEW_WABA_ID = '000000000000001'\nexport const PREVIEW_DISPLAY_PHONE_NUMBER = '+5511900000000'\n\n// O que a Meta manda para áudio gravado no app; o pipeline de STT depende do mime para decodificar.\nexport const PREVIEW_AUDIO_MIME_TYPE = 'audio/ogg; codecs=opus'\n\ntype InboundEnvelopeParams = {\n readonly from: string\n readonly phoneNumberId?: string\n readonly displayPhoneNumber?: string\n readonly wabaId?: string\n}\n\n// `globalThis.crypto` em vez de `node:crypto`: é o que existe tanto no navegador quanto no Node\n// 19+/Bun, e é o que mantém estes builders utilizáveis pelo preview do navegador.\nfunction generateWamid(): string {\n // Formato real é `wamid.<base64>`. O prefixo importa para quem faz parsing por prefixo; o corpo\n // só precisa ser único por entrega, que é o que mantém a assinatura — e o nonce — distinta.\n const unique = globalThis.crypto.randomUUID().replaceAll('-', '')\n return `wamid.${unique}`\n}\n\nfunction currentTimestamp(): string {\n return Math.floor(Date.now() / 1000).toString()\n}\n\ntype BuildEnvelopeParams = InboundEnvelopeParams & {\n readonly message: WhatsAppMessage\n}\n\nfunction buildEnvelope(params: BuildEnvelopeParams): WhatsAppWebhookPayload {\n return {\n object: 'whatsapp_business_account',\n entry: [\n {\n id: params.wabaId ?? PREVIEW_WABA_ID,\n changes: [\n {\n value: {\n messaging_product: 'whatsapp',\n metadata: {\n display_phone_number: params.displayPhoneNumber ?? PREVIEW_DISPLAY_PHONE_NUMBER,\n phone_number_id: params.phoneNumberId ?? PREVIEW_PHONE_NUMBER_ID,\n },\n messages: [params.message],\n },\n },\n ],\n },\n ],\n }\n}\n\nexport type BuildInboundTextPayloadParams = InboundEnvelopeParams & {\n readonly text: string\n}\n\nexport function buildInboundTextPayload(params: BuildInboundTextPayloadParams): WhatsAppWebhookPayload {\n const { text, ...envelope } = params\n\n return buildEnvelope({\n ...envelope,\n message: {\n id: generateWamid(),\n from: params.from,\n type: 'text',\n text: { body: text },\n timestamp: currentTimestamp(),\n },\n })\n}\n\nexport type InteractiveReplyOption = {\n readonly id: string\n readonly title: string\n}\n\n// União exclusiva: uma resposta interativa é de botão OU de lista, nunca das duas nem de nenhuma.\n// O tipo torna o erro impossível em vez de validá-lo em runtime.\nexport type BuildInboundInteractivePayloadParams = InboundEnvelopeParams &\n (\n | { readonly buttonReply: InteractiveReplyOption; readonly listReply?: never }\n | { readonly listReply: InteractiveReplyOption; readonly buttonReply?: never }\n )\n\nexport function buildInboundInteractivePayload(params: BuildInboundInteractivePayloadParams): WhatsAppWebhookPayload {\n const { buttonReply, listReply, ...envelope } = params\n\n const interactive = buttonReply\n ? { type: 'button_reply', button_reply: buttonReply }\n : { type: 'list_reply', list_reply: listReply }\n\n return buildEnvelope({\n ...envelope,\n message: {\n id: generateWamid(),\n from: params.from,\n type: 'interactive',\n interactive,\n timestamp: currentTimestamp(),\n },\n })\n}\n\nexport type BuildInboundAudioPayloadParams = InboundEnvelopeParams & {\n readonly mediaId: string\n readonly mimeType?: string\n}\n\nexport function buildInboundAudioPayload(params: BuildInboundAudioPayloadParams): WhatsAppWebhookPayload {\n const { mediaId, mimeType, ...envelope } = params\n\n return buildEnvelope({\n ...envelope,\n message: {\n id: generateWamid(),\n from: params.from,\n type: 'audio',\n audio: { id: mediaId, mime_type: mimeType ?? PREVIEW_AUDIO_MIME_TYPE },\n timestamp: currentTimestamp(),\n },\n })\n}\n\n/** Tipos de mídia que a Meta entrega por `id`, para o consumidor baixar depois. */\nexport type InboundMediaType = 'image' | 'video' | 'audio' | 'document' | 'sticker'\n\nexport type BuildInboundMediaPayloadParams = InboundEnvelopeParams & {\n readonly mediaType: InboundMediaType\n readonly mediaId: string\n readonly mimeType?: string\n readonly filename?: string\n readonly caption?: string\n}\n\n/**\n * Mídia genérica. O bloco vai com o nome do próprio tipo (`image`, `document`, …) porque é assim que\n * a Meta monta o webhook — e é por esse nome que o consumidor localiza o `id` para baixar o arquivo.\n *\n * `filename` só existe em documento e `caption` só em imagem/vídeo/documento; enviar os dois em\n * áudio geraria um payload que a Meta nunca produz, então eles são omitidos quando vazios.\n */\nexport function buildInboundMediaPayload(params: BuildInboundMediaPayloadParams): WhatsAppWebhookPayload {\n const { mediaType, mediaId, mimeType, filename, caption, ...envelope } = params\n\n return buildEnvelope({\n ...envelope,\n message: {\n id: generateWamid(),\n from: params.from,\n type: mediaType,\n [mediaType]: {\n id: mediaId,\n ...(mimeType ? { mime_type: mimeType } : {}),\n ...(filename ? { filename } : {}),\n ...(caption ? { caption } : {}),\n },\n timestamp: currentTimestamp(),\n },\n })\n}\n\n/**\n * Serializa o payload uma única vez. A validação assina os bytes exatos recebidos: quem reserializa\n * antes de enviar (ou deixa o cliente HTTP serializar o objeto) muda espaçamento/ordem e derruba a\n * assinatura. Assinar e enviar SEMPRE a string devolvida aqui é o contrato.\n */\nexport function serializeWebhookPayload(payload: WhatsAppWebhookPayload): string {\n return JSON.stringify(payload)\n}\n"],"mappings":";;;;AAkBO,IAAMA,0BAA0B;AAChC,IAAMC,kBAAkB;AACxB,IAAMC,+BAA+B;AAGrC,IAAMC,0BAA0B;AAWvC,SAASC,gBAAAA;AAGP,QAAMC,SAASC,WAAWC,OAAOC,WAAU,EAAGC,WAAW,KAAK,EAAA;AAC9D,SAAO,SAASJ,MAAAA;AAClB;AALSD;AAOT,SAASM,mBAAAA;AACP,SAAOC,KAAKC,MAAMC,KAAKC,IAAG,IAAK,GAAA,EAAMC,SAAQ;AAC/C;AAFSL;AAQT,SAASM,cAAcC,QAA2B;AAChD,SAAO;IACLC,QAAQ;IACRC,OAAO;MACL;QACEC,IAAIH,OAAOI,UAAUpB;QACrBqB,SAAS;UACP;YACEC,OAAO;cACLC,mBAAmB;cACnBC,UAAU;gBACRC,sBAAsBT,OAAOU,sBAAsBzB;gBACnD0B,iBAAiBX,OAAOY,iBAAiB7B;cAC3C;cACA8B,UAAU;gBAACb,OAAOc;;YACpB;UACF;;MAEJ;;EAEJ;AACF;AArBSf;AA2BF,SAASgB,wBAAwBf,QAAqC;AAC3E,QAAM,EAAEgB,MAAM,GAAGC,SAAAA,IAAajB;AAE9B,SAAOD,cAAc;IACnB,GAAGkB;IACHH,SAAS;MACPX,IAAIhB,cAAAA;MACJ+B,MAAMlB,OAAOkB;MACbC,MAAM;MACNH,MAAM;QAAEI,MAAMJ;MAAK;MACnBK,WAAW5B,iBAAAA;IACb;EACF,CAAA;AACF;AAbgBsB;AA4BT,SAASO,+BAA+BtB,QAA4C;AACzF,QAAM,EAAEuB,aAAaC,WAAW,GAAGP,SAAAA,IAAajB;AAEhD,QAAMyB,cAAcF,cAChB;IAAEJ,MAAM;IAAgBO,cAAcH;EAAY,IAClD;IAAEJ,MAAM;IAAcQ,YAAYH;EAAU;AAEhD,SAAOzB,cAAc;IACnB,GAAGkB;IACHH,SAAS;MACPX,IAAIhB,cAAAA;MACJ+B,MAAMlB,OAAOkB;MACbC,MAAM;MACNM;MACAJ,WAAW5B,iBAAAA;IACb;EACF,CAAA;AACF;AAjBgB6B;AAwBT,SAASM,yBAAyB5B,QAAsC;AAC7E,QAAM,EAAE6B,SAASC,UAAU,GAAGb,SAAAA,IAAajB;AAE3C,SAAOD,cAAc;IACnB,GAAGkB;IACHH,SAAS;MACPX,IAAIhB,cAAAA;MACJ+B,MAAMlB,OAAOkB;MACbC,MAAM;MACNY,OAAO;QAAE5B,IAAI0B;QAASG,WAAWF,YAAY5C;MAAwB;MACrEmC,WAAW5B,iBAAAA;IACb;EACF,CAAA;AACF;AAbgBmC;AAiCT,SAASK,yBAAyBjC,QAAsC;AAC7E,QAAM,EAAEkC,WAAWL,SAASC,UAAUK,UAAUC,SAAS,GAAGnB,SAAAA,IAAajB;AAEzE,SAAOD,cAAc;IACnB,GAAGkB;IACHH,SAAS;MACPX,IAAIhB,cAAAA;MACJ+B,MAAMlB,OAAOkB;MACbC,MAAMe;MACN,CAACA,SAAAA,GAAY;QACX/B,IAAI0B;QACJ,GAAIC,WAAW;UAAEE,WAAWF;QAAS,IAAI,CAAC;QAC1C,GAAIK,WAAW;UAAEA;QAAS,IAAI,CAAC;QAC/B,GAAIC,UAAU;UAAEA;QAAQ,IAAI,CAAC;MAC/B;MACAf,WAAW5B,iBAAAA;IACb;EACF,CAAA;AACF;AAlBgBwC;AAyBT,SAASI,wBAAwBC,SAA+B;AACrE,SAAOC,KAAKC,UAAUF,OAAAA;AACxB;AAFgBD;","names":["PREVIEW_PHONE_NUMBER_ID","PREVIEW_WABA_ID","PREVIEW_DISPLAY_PHONE_NUMBER","PREVIEW_AUDIO_MIME_TYPE","generateWamid","unique","globalThis","crypto","randomUUID","replaceAll","currentTimestamp","Math","floor","Date","now","toString","buildEnvelope","params","object","entry","id","wabaId","changes","value","messaging_product","metadata","display_phone_number","displayPhoneNumber","phone_number_id","phoneNumberId","messages","message","buildInboundTextPayload","text","envelope","from","type","body","timestamp","buildInboundInteractivePayload","buttonReply","listReply","interactive","button_reply","list_reply","buildInboundAudioPayload","mediaId","mimeType","audio","mime_type","buildInboundMediaPayload","mediaType","filename","caption","serializeWebhookPayload","payload","JSON","stringify"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adatechnology/meta-whatsapp-contracts",
|
|
3
|
-
"version": "0.2.0-rc.
|
|
3
|
+
"version": "0.2.0-rc.7",
|
|
4
4
|
"description": "Shared types, zod schemas, and port interfaces for the meta-whatsapp trio (contracts only — no runtime behavior)",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|