cdp-edge 1.0.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.
Files changed (176) hide show
  1. package/README.md +324 -0
  2. package/bin/cdp-edge.js +71 -0
  3. package/contracts/agent-versions.json +679 -0
  4. package/contracts/api-versions.json +372 -0
  5. package/contracts/types.ts +81 -0
  6. package/dist/commands/analyze.js +52 -0
  7. package/dist/commands/infra.js +54 -0
  8. package/dist/commands/install.js +191 -0
  9. package/dist/commands/server.js +174 -0
  10. package/dist/commands/setup.js +355 -0
  11. package/dist/commands/validate.js +248 -0
  12. package/dist/index.js +12 -0
  13. package/dist/sdk/cdpTrack.js +2095 -0
  14. package/dist/sdk/cdpTrack.min.js +64 -0
  15. package/dist/sdk/install-snippet.html +10 -0
  16. package/docs/CI-CD-SETUP.md +217 -0
  17. package/docs/events-reference.md +359 -0
  18. package/docs/installation.md +155 -0
  19. package/docs/quick-start.md +185 -0
  20. package/docs/sdk-reference.md +371 -0
  21. package/docs/whatsapp-ctwa.md +210 -0
  22. package/extracted-skill/tracking-events-generator/INDEX.md +94 -0
  23. package/extracted-skill/tracking-events-generator/INSTALACAO-CDPEDGE.md +58 -0
  24. package/extracted-skill/tracking-events-generator/INTEGRACAO-COMPLETA.md +683 -0
  25. package/extracted-skill/tracking-events-generator/MELHORIAS-IMPLEMENTADAS.md +513 -0
  26. package/extracted-skill/tracking-events-generator/Premium-Tracking-Intelligence-Resumo.md +333 -0
  27. package/extracted-skill/tracking-events-generator/SKILL.md +257 -0
  28. package/extracted-skill/tracking-events-generator/advanced-matching.js +364 -0
  29. package/extracted-skill/tracking-events-generator/agents/ab-ltv-agent.md +196 -0
  30. package/extracted-skill/tracking-events-generator/agents/ab-testing-agent.md +54 -0
  31. package/extracted-skill/tracking-events-generator/agents/attribution-agent.md +1304 -0
  32. package/extracted-skill/tracking-events-generator/agents/bidding-agent.md +347 -0
  33. package/extracted-skill/tracking-events-generator/agents/bing-agent.md +66 -0
  34. package/extracted-skill/tracking-events-generator/agents/browser-tracking.md +364 -0
  35. package/extracted-skill/tracking-events-generator/agents/code-guardian-agent.md +149 -0
  36. package/extracted-skill/tracking-events-generator/agents/compliance-agent.md +2097 -0
  37. package/extracted-skill/tracking-events-generator/agents/crm-integration-agent.md +1459 -0
  38. package/extracted-skill/tracking-events-generator/agents/dashboard-agent.md +456 -0
  39. package/extracted-skill/tracking-events-generator/agents/database-agent.md +668 -0
  40. package/extracted-skill/tracking-events-generator/agents/debug-agent.md +1455 -0
  41. package/extracted-skill/tracking-events-generator/agents/devops-agent.md +232 -0
  42. package/extracted-skill/tracking-events-generator/agents/domain-setup-agent.md +238 -0
  43. package/extracted-skill/tracking-events-generator/agents/email-agent.md +88 -0
  44. package/extracted-skill/tracking-events-generator/agents/fingerprint-agent.md +257 -0
  45. package/extracted-skill/tracking-events-generator/agents/fraud-detection-agent.md +143 -0
  46. package/extracted-skill/tracking-events-generator/agents/google-agent.md +235 -0
  47. package/extracted-skill/tracking-events-generator/agents/intelligence-agent.md +525 -0
  48. package/extracted-skill/tracking-events-generator/agents/lead-scoring-agent.md +282 -0
  49. package/extracted-skill/tracking-events-generator/agents/linkedin-agent.md +173 -0
  50. package/extracted-skill/tracking-events-generator/agents/localization-agent.md +55 -0
  51. package/extracted-skill/tracking-events-generator/agents/ltv-predictor-agent.md +59 -0
  52. package/extracted-skill/tracking-events-generator/agents/master-feedback-loop.md +960 -0
  53. package/extracted-skill/tracking-events-generator/agents/master-orchestrator.md +2154 -0
  54. package/extracted-skill/tracking-events-generator/agents/match-quality-agent.md +304 -0
  55. package/extracted-skill/tracking-events-generator/agents/memory-agent.json +25 -0
  56. package/extracted-skill/tracking-events-generator/agents/memory-agent.md +878 -0
  57. package/extracted-skill/tracking-events-generator/agents/meta-agent.md +118 -0
  58. package/extracted-skill/tracking-events-generator/agents/ml-clustering-agent.md +749 -0
  59. package/extracted-skill/tracking-events-generator/agents/page-analyzer.md +272 -0
  60. package/extracted-skill/tracking-events-generator/agents/performance-agent.md +1167 -0
  61. package/extracted-skill/tracking-events-generator/agents/performance-optimization-agent.md +1442 -0
  62. package/extracted-skill/tracking-events-generator/agents/pinterest-agent.md +318 -0
  63. package/extracted-skill/tracking-events-generator/agents/premium-tracking-intelligence-agent.md +849 -0
  64. package/extracted-skill/tracking-events-generator/agents/r2-setup-agent.md +258 -0
  65. package/extracted-skill/tracking-events-generator/agents/reddit-agent.md +321 -0
  66. package/extracted-skill/tracking-events-generator/agents/security-enterprise-agent.md +1861 -0
  67. package/extracted-skill/tracking-events-generator/agents/server-tracking.md +1188 -0
  68. package/extracted-skill/tracking-events-generator/agents/spotify-agent.md +391 -0
  69. package/extracted-skill/tracking-events-generator/agents/tiktok-agent.md +182 -0
  70. package/extracted-skill/tracking-events-generator/agents/tracking-plan-agent.md +459 -0
  71. package/extracted-skill/tracking-events-generator/agents/utm-agent.md +322 -0
  72. package/extracted-skill/tracking-events-generator/agents/validator-agent.md +271 -0
  73. package/extracted-skill/tracking-events-generator/agents/webhook-agent.md +177 -0
  74. package/extracted-skill/tracking-events-generator/agents/whatsapp-agent.md +129 -0
  75. package/extracted-skill/tracking-events-generator/agents/whatsapp-ctwa-setup-agent.md +707 -0
  76. package/extracted-skill/tracking-events-generator/agents/youtube-agent.md +537 -0
  77. package/extracted-skill/tracking-events-generator/anti-blocking.js +285 -0
  78. package/extracted-skill/tracking-events-generator/cdpTrack.js +640 -0
  79. package/extracted-skill/tracking-events-generator/contracts/api-versions.json +372 -0
  80. package/extracted-skill/tracking-events-generator/docs/guia-cloudflare-iniciante.md +107 -0
  81. package/extracted-skill/tracking-events-generator/engagement-scoring.js +226 -0
  82. package/extracted-skill/tracking-events-generator/evals/evals.json +235 -0
  83. package/extracted-skill/tracking-events-generator/integration-test.js +497 -0
  84. package/extracted-skill/tracking-events-generator/knowledge-base.md +3066 -0
  85. package/extracted-skill/tracking-events-generator/micro-events.js +992 -0
  86. package/extracted-skill/tracking-events-generator/models/captura-de-lead.md +78 -0
  87. package/extracted-skill/tracking-events-generator/models/captura-lead-evento-externo.md +99 -0
  88. package/extracted-skill/tracking-events-generator/models/checkout-proprio.md +111 -0
  89. package/extracted-skill/tracking-events-generator/models/lancamento-imobiliario.md +344 -0
  90. package/extracted-skill/tracking-events-generator/models/multi-step-checkout.md +672 -0
  91. package/extracted-skill/tracking-events-generator/models/pagina-obrigado.md +55 -0
  92. package/extracted-skill/tracking-events-generator/models/pinterest/conversions-api-template.js +144 -0
  93. package/extracted-skill/tracking-events-generator/models/pinterest/event-mappings.json +48 -0
  94. package/extracted-skill/tracking-events-generator/models/pinterest/tag-template.js +28 -0
  95. package/extracted-skill/tracking-events-generator/models/quiz-funnel.md +132 -0
  96. package/extracted-skill/tracking-events-generator/models/reddit/conversions-api-template.js +205 -0
  97. package/extracted-skill/tracking-events-generator/models/reddit/event-mappings.json +56 -0
  98. package/extracted-skill/tracking-events-generator/models/reddit/pixel-template.js +19 -0
  99. package/extracted-skill/tracking-events-generator/models/scenarios/behavior-engine.js +425 -0
  100. package/extracted-skill/tracking-events-generator/models/scenarios/real-estate-logic.md +50 -0
  101. package/extracted-skill/tracking-events-generator/models/scenarios/sales-page-logic.md +50 -0
  102. package/extracted-skill/tracking-events-generator/models/trafego-direto.md +582 -0
  103. package/extracted-skill/tracking-events-generator/models/webinar-registration.md +63 -0
  104. package/extracted-skill/tracking-events-generator/route-intent-capture.js +222 -0
  105. package/extracted-skill/tracking-events-generator/tracking.config.js +46 -0
  106. package/extracted-skill/tracking-events-generator/walkthrough.md +26 -0
  107. package/package.json +89 -0
  108. package/scripts/build-sdk.js +106 -0
  109. package/server-edge-tracker/.client.env.example +14 -0
  110. package/server-edge-tracker/INSTALAR.md +527 -0
  111. package/server-edge-tracker/SEGMENTATION-DOCS.md +513 -0
  112. package/server-edge-tracker/config/utm-mapping.json +64 -0
  113. package/server-edge-tracker/deploy-client.cjs +76 -0
  114. package/server-edge-tracker/index.ts +1164 -0
  115. package/server-edge-tracker/migrate-new-db.sql +137 -0
  116. package/server-edge-tracker/migrate-v2.sql +16 -0
  117. package/server-edge-tracker/migrate-v3.sql +6 -0
  118. package/server-edge-tracker/migrate-v4.sql +18 -0
  119. package/server-edge-tracker/migrate-v5.sql +17 -0
  120. package/server-edge-tracker/migrate-v6.sql +24 -0
  121. package/server-edge-tracker/migrate-v7.sql +64 -0
  122. package/server-edge-tracker/migrate.sql +111 -0
  123. package/server-edge-tracker/modules/db.ts +702 -0
  124. package/server-edge-tracker/modules/dispatch/ga4.ts +72 -0
  125. package/server-edge-tracker/modules/dispatch/meta.ts +143 -0
  126. package/server-edge-tracker/modules/dispatch/platforms.ts +255 -0
  127. package/server-edge-tracker/modules/dispatch/tiktok.ts +107 -0
  128. package/server-edge-tracker/modules/dispatch/whatsapp.ts +279 -0
  129. package/server-edge-tracker/modules/intelligence.ts +589 -0
  130. package/server-edge-tracker/modules/ml/bidding.ts +247 -0
  131. package/server-edge-tracker/modules/ml/fraud.ts +302 -0
  132. package/server-edge-tracker/modules/ml/logistic.ts +226 -0
  133. package/server-edge-tracker/modules/ml/ltv.ts +531 -0
  134. package/server-edge-tracker/modules/ml/matchquality.ts +232 -0
  135. package/server-edge-tracker/modules/ml/quiz.ts +343 -0
  136. package/server-edge-tracker/modules/ml/roas.ts +255 -0
  137. package/server-edge-tracker/modules/ml/segmentation.ts +407 -0
  138. package/server-edge-tracker/modules/nurture.ts +257 -0
  139. package/server-edge-tracker/modules/utils.ts +311 -0
  140. package/server-edge-tracker/modules/utm/utm-enricher.ts +231 -0
  141. package/server-edge-tracker/schema-ab-ltv.sql +97 -0
  142. package/server-edge-tracker/schema-bidding.sql +86 -0
  143. package/server-edge-tracker/schema-fraud.sql +90 -0
  144. package/server-edge-tracker/schema-indexes.sql +67 -0
  145. package/server-edge-tracker/schema-ltv-feedback.sql +11 -0
  146. package/server-edge-tracker/schema-quiz.sql +52 -0
  147. package/server-edge-tracker/schema-sales-engine.sql +113 -0
  148. package/server-edge-tracker/schema-segmentation.sql +219 -0
  149. package/server-edge-tracker/schema-utm.sql +82 -0
  150. package/server-edge-tracker/schema.sql +265 -0
  151. package/server-edge-tracker/types.ts +258 -0
  152. package/server-edge-tracker/wrangler.toml +136 -0
  153. package/templates/afiliado-sem-landing.md +312 -0
  154. package/templates/captura-de-lead.md +78 -0
  155. package/templates/captura-lead-evento-externo.md +99 -0
  156. package/templates/checkout-proprio.md +111 -0
  157. package/templates/install/.claude/commands/cdp.md +1 -0
  158. package/templates/install/CLAUDE.md +65 -0
  159. package/templates/lancamento-imobiliario.md +344 -0
  160. package/templates/linkedin/tag-template.js +46 -0
  161. package/templates/multi-step-checkout.md +672 -0
  162. package/templates/pagina-obrigado.md +55 -0
  163. package/templates/pinterest/conversions-api-template.js +144 -0
  164. package/templates/pinterest/event-mappings.json +48 -0
  165. package/templates/pinterest/tag-template.js +28 -0
  166. package/templates/quiz-funnel.md +132 -0
  167. package/templates/reddit/conversions-api-template.js +205 -0
  168. package/templates/reddit/event-mappings.json +56 -0
  169. package/templates/reddit/pixel-template.js +19 -0
  170. package/templates/scenarios/behavior-engine.js +425 -0
  171. package/templates/scenarios/real-estate-logic.md +50 -0
  172. package/templates/scenarios/sales-page-logic.md +50 -0
  173. package/templates/spotify/pixel-template.js +46 -0
  174. package/templates/trafego-direto.md +582 -0
  175. package/templates/vsl-page.md +292 -0
  176. package/templates/webinar-registration.md +63 -0
@@ -0,0 +1,279 @@
1
+ /**
2
+ * CDP Edge — WhatsApp Cloud API v22.0 + HMAC Verification
3
+ * sendWhatsApp, processWhatsAppWebhook, verifyHmac, sendCallMeBot
4
+ */
5
+
6
+ import { sha256, normalizePhone } from '../utils.js';
7
+ import { saveLead, logApiFailure } from '../db.js';
8
+ import { Env, TrackPayload } from '../../types.js';
9
+ import { ExecutionContext } from '@cloudflare/workers-types';
10
+
11
+ // ── Tipos ───────────────────────────────────────────────────────────────────────
12
+ interface WhatsAppOptions {
13
+ to?: string;
14
+ template?: {
15
+ name: string;
16
+ language?: string;
17
+ components?: any[];
18
+ };
19
+ mediaType?: 'image' | 'document' | 'video' | 'audio';
20
+ mediaUrl?: string;
21
+ caption?: string;
22
+ filename?: string;
23
+ interactive?: 'buttons' | 'list';
24
+ bodyText?: string;
25
+ buttons?: Array<{ id: string; title: string }>;
26
+ listButton?: string;
27
+ rows?: Array<{ id: string; title: string; description?: string }>;
28
+ }
29
+
30
+ interface WhatsAppMessage {
31
+ from: string;
32
+ id: string;
33
+ type: string;
34
+ text?: { body: string };
35
+ referral?: {
36
+ ctwa_clid?: string;
37
+ source_id?: string;
38
+ source_url?: string;
39
+ headline?: string;
40
+ };
41
+ }
42
+
43
+ // ── Resolvedores de secrets (canônico + legado) ────────────────────────────────
44
+ // Meta Cloud API v22.0 usa PHONE_NUMBER_ID e ACCESS_TOKEN como termos oficiais.
45
+ // Suportamos ambos os nomes para compatibilidade com secrets já configurados.
46
+ function resolvePhoneNumberId(env: Env): string | undefined {
47
+ return env.WHATSAPP_PHONE_NUMBER_ID || env.WA_PHONE_ID;
48
+ }
49
+ function resolveAccessToken(env: Env): string | undefined {
50
+ return env.WHATSAPP_ACCESS_TOKEN || env.WA_ACCESS_TOKEN;
51
+ }
52
+
53
+ // ── sendWhatsApp — envia mensagem via Meta Cloud API ──────────────────────────
54
+ export async function sendWhatsApp(env: Env, tipo: string, payload: TrackPayload, options: WhatsAppOptions = {}): Promise<any> {
55
+ if (!resolvePhoneNumberId(env) || !resolveAccessToken(env) || !env.WA_NOTIFY_NUMBER) {
56
+ return { skipped: 'WhatsApp não configurado' };
57
+ }
58
+
59
+ const to = options.to || env.WA_NOTIFY_NUMBER;
60
+
61
+ if (options.template) {
62
+ const { name, language = 'pt_BR', components = [] } = options.template;
63
+ const body = { messaging_product: 'whatsapp', to, type: 'template', template: { name, language: { code: language }, components } };
64
+ return _sendWARequest(env, body);
65
+ }
66
+
67
+ if (options.mediaType && options.mediaUrl) {
68
+ const mediaPayload: Record<string, string> = { link: options.mediaUrl };
69
+ if (options.caption) mediaPayload.caption = options.caption;
70
+ if (options.filename) mediaPayload.filename = options.filename;
71
+ const body = { messaging_product: 'whatsapp', to, type: options.mediaType, [options.mediaType]: mediaPayload };
72
+ return _sendWARequest(env, body);
73
+ }
74
+
75
+ if (options.interactive === 'buttons' && options.buttons && options.buttons.length > 0) {
76
+ const body = {
77
+ messaging_product: 'whatsapp', to, type: 'interactive',
78
+ interactive: {
79
+ type: 'button',
80
+ body: { text: options.bodyText || '' },
81
+ action: {
82
+ buttons: options.buttons.slice(0, 3).map(b => ({ type: 'reply', reply: { id: b.id, title: b.title } })),
83
+ },
84
+ },
85
+ };
86
+ return _sendWARequest(env, body);
87
+ }
88
+
89
+ if (options.interactive === 'list' && options.rows && options.rows.length > 0) {
90
+ const body = {
91
+ messaging_product: 'whatsapp', to, type: 'interactive',
92
+ interactive: {
93
+ type: 'list',
94
+ body: { text: options.bodyText || '' },
95
+ action: { button: options.listButton || 'Ver opções', sections: [{ rows: options.rows.slice(0, 10) }] },
96
+ },
97
+ };
98
+ return _sendWARequest(env, body);
99
+ }
100
+
101
+ // Text fallback (dentro da janela de 24h)
102
+ const nome = [payload.firstName, payload.lastName].filter(Boolean).join(' ') || 'sem nome';
103
+ const valor = payload.value ? `R$ ${parseFloat(String(payload.value)).toFixed(2)}` : '—';
104
+ const utm = payload.utmSource || 'direto';
105
+ const produto = payload.contentName || '';
106
+
107
+ let texto = '';
108
+ if (tipo === 'Purchase') {
109
+ texto =
110
+ `🛒 *Nova Venda!*\n\n` +
111
+ `👤 ${nome}\n📧 ${payload.email || '—'}\n📱 ${payload.phone || '—'}\n` +
112
+ `💰 ${valor}\n${produto ? `📦 ${produto}\n` : ''}` +
113
+ `🔗 UTM: ${utm}\n🕐 ${new Date().toLocaleString('pt-BR', { timeZone: 'America/Sao_Paulo' })}`;
114
+ } else if (tipo === 'Lead') {
115
+ texto =
116
+ `📋 *Novo Lead!*\n\n` +
117
+ `📧 ${payload.email || '—'}\n🔗 UTM: ${utm}\n` +
118
+ `🌐 ${payload.pageUrl || '—'}\n🕐 ${new Date().toLocaleString('pt-BR', { timeZone: 'America/Sao_Paulo' })}`;
119
+ } else {
120
+ return { skipped: `tipo ${tipo} não suportado sem template` };
121
+ }
122
+
123
+ return _sendWARequest(env, { messaging_product: 'whatsapp', to, type: 'text', text: { body: texto } });
124
+ }
125
+
126
+ // ── _sendWARequest — executor interno ─────────────────────────────────────────
127
+ async function _sendWARequest(env: Env, body: Record<string, any>): Promise<any> {
128
+ try {
129
+ const phoneNumberId = resolvePhoneNumberId(env);
130
+ const accessToken = resolveAccessToken(env);
131
+ const res = await fetch(`https://graph.facebook.com/v22.0/${phoneNumberId}/messages`, {
132
+ method: 'POST',
133
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}` },
134
+ body: JSON.stringify(body),
135
+ });
136
+ const data = await res.json();
137
+ if (!res.ok) console.error('WhatsApp Meta API error:', res.status, (data as any).error?.message || 'unknown');
138
+ return { ok: res.ok, status: res.status, data };
139
+ } catch (err: any) {
140
+ console.error('WhatsApp Meta API failed:', err?.message || String(err));
141
+ return { ok: false, error: err?.message || String(err) };
142
+ }
143
+ }
144
+
145
+ // ── sendCallMeBot — alertas de sistema via WhatsApp ───────────────────────────
146
+ export async function sendCallMeBot(env: Env, mensagem: string): Promise<any> {
147
+ if (!env.CALLMEBOT_PHONE || !env.CALLMEBOT_APIKEY) {
148
+ return { skipped: 'CallMeBot não configurado' };
149
+ }
150
+ try {
151
+ const url = `https://api.callmebot.com/whatsapp.php?phone=${encodeURIComponent(env.CALLMEBOT_PHONE)}&text=${encodeURIComponent(mensagem)}&apikey=${env.CALLMEBOT_APIKEY}`;
152
+ const res = await fetch(url);
153
+ return { ok: res.ok, status: res.status };
154
+ } catch (err: any) {
155
+ console.error('CallMeBot failed:', err?.message || String(err));
156
+ return { ok: false, error: err?.message || String(err) };
157
+ }
158
+ }
159
+
160
+ // ── processWhatsAppWebhook — CTWA (Click to WhatsApp) ────────────────────────
161
+ export async function processWhatsAppWebhook(env: Env, body: any, request: Request, ctx: ExecutionContext): Promise<any> {
162
+ const entry: any = body?.entry?.[0];
163
+ const change = entry?.changes?.find((c: any) => c.field === 'messages');
164
+ if (!change) return { skipped: 'no messages field' };
165
+
166
+ const messages = change.value?.messages;
167
+ if (!messages || messages.length === 0) return { skipped: 'no messages' };
168
+
169
+ const results: any[] = [];
170
+
171
+ for (const message of messages) {
172
+ const phone = message.from;
173
+ const wamid = message.id;
174
+ const referral = message.referral || {};
175
+ const ctwaClid = referral.ctwa_clid || null;
176
+ const adId = referral.source_id || null;
177
+ const sourceUrl = referral.source_url || null;
178
+ const headline = referral.headline || null;
179
+ const messageBody = message.text?.body || message.type || '';
180
+
181
+ if (!phone) { results.push({ skipped: 'no phone' }); continue; }
182
+
183
+ const phoneNorm = normalizePhone(phone) || phone;
184
+ const phoneHash = await sha256(phoneNorm);
185
+
186
+ // Deduplicação por wamid
187
+ if (env.DB && wamid) {
188
+ try {
189
+ const existing = await env.DB.prepare('SELECT id FROM whatsapp_contacts WHERE wamid = ?').bind(wamid).first();
190
+ if (existing) { results.push({ skipped: 'duplicate wamid', wamid }); continue; }
191
+ } catch { /* não bloquear se D1 falhar */ }
192
+ }
193
+
194
+ const eventId = `ctwa_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
195
+
196
+ if (env.DB) {
197
+ ctx.waitUntil(
198
+ env.DB.prepare(
199
+ `INSERT OR IGNORE INTO whatsapp_contacts (phone_hash, phone_raw, wamid, ctwa_clid, ad_id, source_url, headline, capi_sent, capi_event_id, message_body) VALUES (?,?,?,?,?,?,?,0,?,?)`
200
+ ).bind(phoneHash, phoneNorm, wamid || null, ctwaClid, adId, sourceUrl, headline, eventId, messageBody || null).run()
201
+ );
202
+ }
203
+
204
+ const capiEvent: Record<string, any> = {
205
+ event_name: 'Contact',
206
+ event_time: Math.floor(Date.now() / 1000),
207
+ event_id: eventId,
208
+ action_source: 'chat',
209
+ user_data: {
210
+ ph: phoneHash,
211
+ ...(ctwaClid && { ctwa_clid: ctwaClid }),
212
+ client_ip_address: request.headers.get('CF-Connecting-IP') || '',
213
+ client_user_agent: request.headers.get('User-Agent') || '',
214
+ },
215
+ ...(sourceUrl && { event_source_url: sourceUrl }),
216
+ };
217
+
218
+ ctx.waitUntil(
219
+ (async () => {
220
+ try {
221
+ const requestBody: Record<string, any> = { data: [capiEvent], access_token: env.META_ACCESS_TOKEN };
222
+ if (env.META_TEST_CODE) requestBody.test_event_code = env.META_TEST_CODE;
223
+
224
+ const res = await fetch(
225
+ `https://graph.facebook.com/v22.0/${env.META_PIXEL_ID}/events`,
226
+ { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }
227
+ );
228
+ const data = await res.json();
229
+
230
+ if (res.ok && env.DB && wamid) {
231
+ await env.DB.prepare('UPDATE whatsapp_contacts SET capi_sent = 1 WHERE wamid = ?').bind(wamid).run();
232
+ } else if (!res.ok) {
233
+ console.error('[CTWA] Meta CAPI error:', res.status, (data as any).error?.message || 'unknown');
234
+ if (env.DB) {
235
+ await logApiFailure(env.DB, 'meta', 'Contact', (data as any).error?.code || res.status, (data as any).error?.message || 'CTWA CAPI error', eventId, JSON.stringify(requestBody));
236
+ }
237
+ }
238
+ } catch (err: any) {
239
+ console.error('[CTWA] Meta CAPI fetch failed:', err?.message || String(err));
240
+ }
241
+ })()
242
+ );
243
+
244
+ ctx.waitUntil(
245
+ saveLead(env, 'Contact', {
246
+ phone: phoneNorm, eventId, pageUrl: sourceUrl,
247
+ utmSource: 'whatsapp_ctwa', utmMedium: 'paid_social',
248
+ }, request, 'whatsapp')
249
+ );
250
+
251
+ results.push({ ok: true, phone: phoneNorm.slice(0, 4) + '****', ctwa_clid: ctwaClid ? 'present' : 'absent', event_id: eventId });
252
+ }
253
+
254
+ return { processed: results.length, results };
255
+ }
256
+
257
+ // ── verifyHmac — validação constant-time de assinatura HMAC-SHA256 ─────────────
258
+ export async function verifyHmac(secret: string, rawBody: string, receivedSignature: string): Promise<boolean> {
259
+ if (!secret || !receivedSignature) return false;
260
+ try {
261
+ const key = await crypto.subtle.importKey(
262
+ 'raw',
263
+ new TextEncoder().encode(secret),
264
+ { name: 'HMAC', hash: 'SHA-256' },
265
+ false,
266
+ ['sign']
267
+ );
268
+ const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(rawBody));
269
+ const computed = Array.from(new Uint8Array(sig)).map(b => b.toString(16).padStart(2, '0')).join('');
270
+ if (computed.length !== receivedSignature.length) return false;
271
+ let diff = 0;
272
+ for (let i = 0; i < computed.length; i++) {
273
+ diff |= computed.charCodeAt(i) ^ receivedSignature.charCodeAt(i);
274
+ }
275
+ return diff === 0;
276
+ } catch {
277
+ return false;
278
+ }
279
+ }