@adatechnology/meta-whatsapp-contracts 0.3.0 → 0.4.0
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 +11 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/testing/index.cjs +25 -0
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +9 -2
- package/dist/testing/index.d.ts +9 -2
- package/dist/testing/index.js +24 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/{webhook.types-DzSMUGwM.d.cts → webhook.types-BsYM6OfC.d.cts} +407 -1
- package/dist/{webhook.types-DzSMUGwM.d.ts → webhook.types-BsYM6OfC.d.ts} +407 -1
- package/package.json +1 -1
|
@@ -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 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 * Nome de perfil de quem manda, como a Meta o entrega em `contacts`.\n *\n * Ausente = webhook sem `contacts`, que é payload legítimo e precisa continuar sendo testável:\n * um builder que sempre injetasse contato esconderia o caminho em que o nome não vem.\n */\n readonly profileName?: 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 ...(params.profileName\n ? { contacts: [{ wa_id: params.from, profile: { name: params.profileName } }] }\n : {}),\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;AAkBvC,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;;cAClB,GAAId,OAAOe,cACP;gBAAEC,UAAU;kBAAC;oBAAEC,OAAOjB,OAAOkB;oBAAMC,SAAS;sBAAEC,MAAMpB,OAAOe;oBAAY;kBAAE;;cAAG,IAC5E,CAAC;YACP;UACF;;MAEJ;;EAEJ;AACF;AAxBShB;AA8BF,SAASsB,wBAAwBrB,QAAqC;AAC3E,QAAM,EAAEsB,MAAM,GAAGC,SAAAA,IAAavB;AAE9B,SAAOD,cAAc;IACnB,GAAGwB;IACHT,SAAS;MACPX,IAAIhB,cAAAA;MACJ+B,MAAMlB,OAAOkB;MACbM,MAAM;MACNF,MAAM;QAAEG,MAAMH;MAAK;MACnBI,WAAWjC,iBAAAA;IACb;EACF,CAAA;AACF;AAbgB4B;AA4BT,SAASM,+BAA+B3B,QAA4C;AACzF,QAAM,EAAE4B,aAAaC,WAAW,GAAGN,SAAAA,IAAavB;AAEhD,QAAM8B,cAAcF,cAChB;IAAEJ,MAAM;IAAgBO,cAAcH;EAAY,IAClD;IAAEJ,MAAM;IAAcQ,YAAYH;EAAU;AAEhD,SAAO9B,cAAc;IACnB,GAAGwB;IACHT,SAAS;MACPX,IAAIhB,cAAAA;MACJ+B,MAAMlB,OAAOkB;MACbM,MAAM;MACNM;MACAJ,WAAWjC,iBAAAA;IACb;EACF,CAAA;AACF;AAjBgBkC;AAwBT,SAASM,yBAAyBjC,QAAsC;AAC7E,QAAM,EAAEkC,SAASC,UAAU,GAAGZ,SAAAA,IAAavB;AAE3C,SAAOD,cAAc;IACnB,GAAGwB;IACHT,SAAS;MACPX,IAAIhB,cAAAA;MACJ+B,MAAMlB,OAAOkB;MACbM,MAAM;MACNY,OAAO;QAAEjC,IAAI+B;QAASG,WAAWF,YAAYjD;MAAwB;MACrEwC,WAAWjC,iBAAAA;IACb;EACF,CAAA;AACF;AAbgBwC;AAiCT,SAASK,yBAAyBtC,QAAsC;AAC7E,QAAM,EAAEuC,WAAWL,SAASC,UAAUK,UAAUC,SAAS,GAAGlB,SAAAA,IAAavB;AAEzE,SAAOD,cAAc;IACnB,GAAGwB;IACHT,SAAS;MACPX,IAAIhB,cAAAA;MACJ+B,MAAMlB,OAAOkB;MACbM,MAAMe;MACN,CAACA,SAAAA,GAAY;QACXpC,IAAI+B;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,WAAWjC,iBAAAA;IACb;EACF,CAAA;AACF;AAlBgB6C;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","profileName","contacts","wa_id","from","profile","name","buildInboundTextPayload","text","envelope","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"]}
|
|
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 buildInboundLocationPayload,\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 BuildInboundLocationPayloadParams,\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 * Nome de perfil de quem manda, como a Meta o entrega em `contacts`.\n *\n * Ausente = webhook sem `contacts`, que é payload legítimo e precisa continuar sendo testável:\n * um builder que sempre injetasse contato esconderia o caminho em que o nome não vem.\n */\n readonly profileName?: 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 ...(params.profileName\n ? { contacts: [{ wa_id: params.from, profile: { name: params.profileName } }] }\n : {}),\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\nexport type BuildInboundLocationPayloadParams = InboundEnvelopeParams & {\n readonly latitude: number\n readonly longitude: number\n readonly name?: string\n readonly address?: string\n}\n\nexport function buildInboundLocationPayload(params: BuildInboundLocationPayloadParams): WhatsAppWebhookPayload {\n const { latitude, longitude, name, address, ...envelope } = params\n\n return buildEnvelope({\n ...envelope,\n message: {\n id: generateWamid(),\n from: params.from,\n type: 'location',\n location: {\n latitude,\n longitude,\n ...(name ? { name } : {}),\n ...(address ? { address } : {}),\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":"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","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","profileName","contacts","wa_id","from","profile","name","buildInboundTextPayload","text","envelope","type","body","timestamp","buildInboundInteractivePayload","buttonReply","listReply","interactive","button_reply","list_reply","buildInboundAudioPayload","mediaId","mimeType","audio","mime_type","buildInboundMediaPayload","mediaType","filename","caption","buildInboundLocationPayload","latitude","longitude","address","location","serializeWebhookPayload","payload","JSON","stringify"]}
|
package/dist/testing/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { p as WhatsAppWebhookPayload } from '../webhook.types-BsYM6OfC.cjs';
|
|
2
2
|
import 'zod';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -71,6 +71,13 @@ type BuildInboundMediaPayloadParams = InboundEnvelopeParams & {
|
|
|
71
71
|
* áudio geraria um payload que a Meta nunca produz, então eles são omitidos quando vazios.
|
|
72
72
|
*/
|
|
73
73
|
declare function buildInboundMediaPayload(params: BuildInboundMediaPayloadParams): WhatsAppWebhookPayload;
|
|
74
|
+
type BuildInboundLocationPayloadParams = InboundEnvelopeParams & {
|
|
75
|
+
readonly latitude: number;
|
|
76
|
+
readonly longitude: number;
|
|
77
|
+
readonly name?: string;
|
|
78
|
+
readonly address?: string;
|
|
79
|
+
};
|
|
80
|
+
declare function buildInboundLocationPayload(params: BuildInboundLocationPayloadParams): WhatsAppWebhookPayload;
|
|
74
81
|
/**
|
|
75
82
|
* Serializa o payload uma única vez. A validação assina os bytes exatos recebidos: quem reserializa
|
|
76
83
|
* antes de enviar (ou deixa o cliente HTTP serializar o objeto) muda espaçamento/ordem e derruba a
|
|
@@ -78,4 +85,4 @@ declare function buildInboundMediaPayload(params: BuildInboundMediaPayloadParams
|
|
|
78
85
|
*/
|
|
79
86
|
declare function serializeWebhookPayload(payload: WhatsAppWebhookPayload): string;
|
|
80
87
|
|
|
81
|
-
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 };
|
|
88
|
+
export { type BuildInboundAudioPayloadParams, type BuildInboundInteractivePayloadParams, type BuildInboundLocationPayloadParams, 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, buildInboundLocationPayload, buildInboundMediaPayload, buildInboundTextPayload, serializeWebhookPayload };
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { p as WhatsAppWebhookPayload } from '../webhook.types-BsYM6OfC.js';
|
|
2
2
|
import 'zod';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -71,6 +71,13 @@ type BuildInboundMediaPayloadParams = InboundEnvelopeParams & {
|
|
|
71
71
|
* áudio geraria um payload que a Meta nunca produz, então eles são omitidos quando vazios.
|
|
72
72
|
*/
|
|
73
73
|
declare function buildInboundMediaPayload(params: BuildInboundMediaPayloadParams): WhatsAppWebhookPayload;
|
|
74
|
+
type BuildInboundLocationPayloadParams = InboundEnvelopeParams & {
|
|
75
|
+
readonly latitude: number;
|
|
76
|
+
readonly longitude: number;
|
|
77
|
+
readonly name?: string;
|
|
78
|
+
readonly address?: string;
|
|
79
|
+
};
|
|
80
|
+
declare function buildInboundLocationPayload(params: BuildInboundLocationPayloadParams): WhatsAppWebhookPayload;
|
|
74
81
|
/**
|
|
75
82
|
* Serializa o payload uma única vez. A validação assina os bytes exatos recebidos: quem reserializa
|
|
76
83
|
* antes de enviar (ou deixa o cliente HTTP serializar o objeto) muda espaçamento/ordem e derruba a
|
|
@@ -78,4 +85,4 @@ declare function buildInboundMediaPayload(params: BuildInboundMediaPayloadParams
|
|
|
78
85
|
*/
|
|
79
86
|
declare function serializeWebhookPayload(payload: WhatsAppWebhookPayload): string;
|
|
80
87
|
|
|
81
|
-
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 };
|
|
88
|
+
export { type BuildInboundAudioPayloadParams, type BuildInboundInteractivePayloadParams, type BuildInboundLocationPayloadParams, 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, buildInboundLocationPayload, buildInboundMediaPayload, buildInboundTextPayload, serializeWebhookPayload };
|
package/dist/testing/index.js
CHANGED
|
@@ -129,6 +129,29 @@ function buildInboundMediaPayload(params) {
|
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
131
|
__name(buildInboundMediaPayload, "buildInboundMediaPayload");
|
|
132
|
+
function buildInboundLocationPayload(params) {
|
|
133
|
+
const { latitude, longitude, name, address, ...envelope } = params;
|
|
134
|
+
return buildEnvelope({
|
|
135
|
+
...envelope,
|
|
136
|
+
message: {
|
|
137
|
+
id: generateWamid(),
|
|
138
|
+
from: params.from,
|
|
139
|
+
type: "location",
|
|
140
|
+
location: {
|
|
141
|
+
latitude,
|
|
142
|
+
longitude,
|
|
143
|
+
...name ? {
|
|
144
|
+
name
|
|
145
|
+
} : {},
|
|
146
|
+
...address ? {
|
|
147
|
+
address
|
|
148
|
+
} : {}
|
|
149
|
+
},
|
|
150
|
+
timestamp: currentTimestamp()
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
__name(buildInboundLocationPayload, "buildInboundLocationPayload");
|
|
132
155
|
function serializeWebhookPayload(payload) {
|
|
133
156
|
return JSON.stringify(payload);
|
|
134
157
|
}
|
|
@@ -140,6 +163,7 @@ export {
|
|
|
140
163
|
PREVIEW_WABA_ID,
|
|
141
164
|
buildInboundAudioPayload,
|
|
142
165
|
buildInboundInteractivePayload,
|
|
166
|
+
buildInboundLocationPayload,
|
|
143
167
|
buildInboundMediaPayload,
|
|
144
168
|
buildInboundTextPayload,
|
|
145
169
|
serializeWebhookPayload
|
|
@@ -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 * Nome de perfil de quem manda, como a Meta o entrega em `contacts`.\n *\n * Ausente = webhook sem `contacts`, que é payload legítimo e precisa continuar sendo testável:\n * um builder que sempre injetasse contato esconderia o caminho em que o nome não vem.\n */\n readonly profileName?: 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 ...(params.profileName\n ? { contacts: [{ wa_id: params.from, profile: { name: params.profileName } }] }\n : {}),\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;AAkBvC,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;;cAClB,GAAId,OAAOe,cACP;gBAAEC,UAAU;kBAAC;oBAAEC,OAAOjB,OAAOkB;oBAAMC,SAAS;sBAAEC,MAAMpB,OAAOe;oBAAY;kBAAE;;cAAG,IAC5E,CAAC;YACP;UACF;;MAEJ;;EAEJ;AACF;AAxBShB;AA8BF,SAASsB,wBAAwBrB,QAAqC;AAC3E,QAAM,EAAEsB,MAAM,GAAGC,SAAAA,IAAavB;AAE9B,SAAOD,cAAc;IACnB,GAAGwB;IACHT,SAAS;MACPX,IAAIhB,cAAAA;MACJ+B,MAAMlB,OAAOkB;MACbM,MAAM;MACNF,MAAM;QAAEG,MAAMH;MAAK;MACnBI,WAAWjC,iBAAAA;IACb;EACF,CAAA;AACF;AAbgB4B;AA4BT,SAASM,+BAA+B3B,QAA4C;AACzF,QAAM,EAAE4B,aAAaC,WAAW,GAAGN,SAAAA,IAAavB;AAEhD,QAAM8B,cAAcF,cAChB;IAAEJ,MAAM;IAAgBO,cAAcH;EAAY,IAClD;IAAEJ,MAAM;IAAcQ,YAAYH;EAAU;AAEhD,SAAO9B,cAAc;IACnB,GAAGwB;IACHT,SAAS;MACPX,IAAIhB,cAAAA;MACJ+B,MAAMlB,OAAOkB;MACbM,MAAM;MACNM;MACAJ,WAAWjC,iBAAAA;IACb;EACF,CAAA;AACF;AAjBgBkC;AAwBT,SAASM,yBAAyBjC,QAAsC;AAC7E,QAAM,EAAEkC,SAASC,UAAU,GAAGZ,SAAAA,IAAavB;AAE3C,SAAOD,cAAc;IACnB,GAAGwB;IACHT,SAAS;MACPX,IAAIhB,cAAAA;MACJ+B,MAAMlB,OAAOkB;MACbM,MAAM;MACNY,OAAO;QAAEjC,IAAI+B;QAASG,WAAWF,YAAYjD;MAAwB;MACrEwC,WAAWjC,iBAAAA;IACb;EACF,CAAA;AACF;AAbgBwC;AAiCT,SAASK,yBAAyBtC,QAAsC;AAC7E,QAAM,EAAEuC,WAAWL,SAASC,UAAUK,UAAUC,SAAS,GAAGlB,SAAAA,IAAavB;AAEzE,SAAOD,cAAc;IACnB,GAAGwB;IACHT,SAAS;MACPX,IAAIhB,cAAAA;MACJ+B,MAAMlB,OAAOkB;MACbM,MAAMe;MACN,CAACA,SAAAA,GAAY;QACXpC,IAAI+B;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,WAAWjC,iBAAAA;IACb;EACF,CAAA;AACF;AAlBgB6C;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","profileName","contacts","wa_id","from","profile","name","buildInboundTextPayload","text","envelope","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"]}
|
|
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 * Nome de perfil de quem manda, como a Meta o entrega em `contacts`.\n *\n * Ausente = webhook sem `contacts`, que é payload legítimo e precisa continuar sendo testável:\n * um builder que sempre injetasse contato esconderia o caminho em que o nome não vem.\n */\n readonly profileName?: 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 ...(params.profileName\n ? { contacts: [{ wa_id: params.from, profile: { name: params.profileName } }] }\n : {}),\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\nexport type BuildInboundLocationPayloadParams = InboundEnvelopeParams & {\n readonly latitude: number\n readonly longitude: number\n readonly name?: string\n readonly address?: string\n}\n\nexport function buildInboundLocationPayload(params: BuildInboundLocationPayloadParams): WhatsAppWebhookPayload {\n const { latitude, longitude, name, address, ...envelope } = params\n\n return buildEnvelope({\n ...envelope,\n message: {\n id: generateWamid(),\n from: params.from,\n type: 'location',\n location: {\n latitude,\n longitude,\n ...(name ? { name } : {}),\n ...(address ? { address } : {}),\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":"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","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","profileName","contacts","wa_id","from","profile","name","buildInboundTextPayload","text","envelope","type","body","timestamp","buildInboundInteractivePayload","buttonReply","listReply","interactive","button_reply","list_reply","buildInboundAudioPayload","mediaId","mimeType","audio","mime_type","buildInboundMediaPayload","mediaType","filename","caption","buildInboundLocationPayload","latitude","longitude","address","location","serializeWebhookPayload","payload","JSON","stringify"]}
|