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.
- package/README.md +324 -0
- package/bin/cdp-edge.js +71 -0
- package/contracts/agent-versions.json +679 -0
- package/contracts/api-versions.json +372 -0
- package/contracts/types.ts +81 -0
- package/dist/commands/analyze.js +52 -0
- package/dist/commands/infra.js +54 -0
- package/dist/commands/install.js +191 -0
- package/dist/commands/server.js +174 -0
- package/dist/commands/setup.js +355 -0
- package/dist/commands/validate.js +248 -0
- package/dist/index.js +12 -0
- package/dist/sdk/cdpTrack.js +2095 -0
- package/dist/sdk/cdpTrack.min.js +64 -0
- package/dist/sdk/install-snippet.html +10 -0
- package/docs/CI-CD-SETUP.md +217 -0
- package/docs/events-reference.md +359 -0
- package/docs/installation.md +155 -0
- package/docs/quick-start.md +185 -0
- package/docs/sdk-reference.md +371 -0
- package/docs/whatsapp-ctwa.md +210 -0
- package/extracted-skill/tracking-events-generator/INDEX.md +94 -0
- package/extracted-skill/tracking-events-generator/INSTALACAO-CDPEDGE.md +58 -0
- package/extracted-skill/tracking-events-generator/INTEGRACAO-COMPLETA.md +683 -0
- package/extracted-skill/tracking-events-generator/MELHORIAS-IMPLEMENTADAS.md +513 -0
- package/extracted-skill/tracking-events-generator/Premium-Tracking-Intelligence-Resumo.md +333 -0
- package/extracted-skill/tracking-events-generator/SKILL.md +257 -0
- package/extracted-skill/tracking-events-generator/advanced-matching.js +364 -0
- package/extracted-skill/tracking-events-generator/agents/ab-ltv-agent.md +196 -0
- package/extracted-skill/tracking-events-generator/agents/ab-testing-agent.md +54 -0
- package/extracted-skill/tracking-events-generator/agents/attribution-agent.md +1304 -0
- package/extracted-skill/tracking-events-generator/agents/bidding-agent.md +347 -0
- package/extracted-skill/tracking-events-generator/agents/bing-agent.md +66 -0
- package/extracted-skill/tracking-events-generator/agents/browser-tracking.md +364 -0
- package/extracted-skill/tracking-events-generator/agents/code-guardian-agent.md +149 -0
- package/extracted-skill/tracking-events-generator/agents/compliance-agent.md +2097 -0
- package/extracted-skill/tracking-events-generator/agents/crm-integration-agent.md +1459 -0
- package/extracted-skill/tracking-events-generator/agents/dashboard-agent.md +456 -0
- package/extracted-skill/tracking-events-generator/agents/database-agent.md +668 -0
- package/extracted-skill/tracking-events-generator/agents/debug-agent.md +1455 -0
- package/extracted-skill/tracking-events-generator/agents/devops-agent.md +232 -0
- package/extracted-skill/tracking-events-generator/agents/domain-setup-agent.md +238 -0
- package/extracted-skill/tracking-events-generator/agents/email-agent.md +88 -0
- package/extracted-skill/tracking-events-generator/agents/fingerprint-agent.md +257 -0
- package/extracted-skill/tracking-events-generator/agents/fraud-detection-agent.md +143 -0
- package/extracted-skill/tracking-events-generator/agents/google-agent.md +235 -0
- package/extracted-skill/tracking-events-generator/agents/intelligence-agent.md +525 -0
- package/extracted-skill/tracking-events-generator/agents/lead-scoring-agent.md +282 -0
- package/extracted-skill/tracking-events-generator/agents/linkedin-agent.md +173 -0
- package/extracted-skill/tracking-events-generator/agents/localization-agent.md +55 -0
- package/extracted-skill/tracking-events-generator/agents/ltv-predictor-agent.md +59 -0
- package/extracted-skill/tracking-events-generator/agents/master-feedback-loop.md +960 -0
- package/extracted-skill/tracking-events-generator/agents/master-orchestrator.md +2154 -0
- package/extracted-skill/tracking-events-generator/agents/match-quality-agent.md +304 -0
- package/extracted-skill/tracking-events-generator/agents/memory-agent.json +25 -0
- package/extracted-skill/tracking-events-generator/agents/memory-agent.md +878 -0
- package/extracted-skill/tracking-events-generator/agents/meta-agent.md +118 -0
- package/extracted-skill/tracking-events-generator/agents/ml-clustering-agent.md +749 -0
- package/extracted-skill/tracking-events-generator/agents/page-analyzer.md +272 -0
- package/extracted-skill/tracking-events-generator/agents/performance-agent.md +1167 -0
- package/extracted-skill/tracking-events-generator/agents/performance-optimization-agent.md +1442 -0
- package/extracted-skill/tracking-events-generator/agents/pinterest-agent.md +318 -0
- package/extracted-skill/tracking-events-generator/agents/premium-tracking-intelligence-agent.md +849 -0
- package/extracted-skill/tracking-events-generator/agents/r2-setup-agent.md +258 -0
- package/extracted-skill/tracking-events-generator/agents/reddit-agent.md +321 -0
- package/extracted-skill/tracking-events-generator/agents/security-enterprise-agent.md +1861 -0
- package/extracted-skill/tracking-events-generator/agents/server-tracking.md +1188 -0
- package/extracted-skill/tracking-events-generator/agents/spotify-agent.md +391 -0
- package/extracted-skill/tracking-events-generator/agents/tiktok-agent.md +182 -0
- package/extracted-skill/tracking-events-generator/agents/tracking-plan-agent.md +459 -0
- package/extracted-skill/tracking-events-generator/agents/utm-agent.md +322 -0
- package/extracted-skill/tracking-events-generator/agents/validator-agent.md +271 -0
- package/extracted-skill/tracking-events-generator/agents/webhook-agent.md +177 -0
- package/extracted-skill/tracking-events-generator/agents/whatsapp-agent.md +129 -0
- package/extracted-skill/tracking-events-generator/agents/whatsapp-ctwa-setup-agent.md +707 -0
- package/extracted-skill/tracking-events-generator/agents/youtube-agent.md +537 -0
- package/extracted-skill/tracking-events-generator/anti-blocking.js +285 -0
- package/extracted-skill/tracking-events-generator/cdpTrack.js +640 -0
- package/extracted-skill/tracking-events-generator/contracts/api-versions.json +372 -0
- package/extracted-skill/tracking-events-generator/docs/guia-cloudflare-iniciante.md +107 -0
- package/extracted-skill/tracking-events-generator/engagement-scoring.js +226 -0
- package/extracted-skill/tracking-events-generator/evals/evals.json +235 -0
- package/extracted-skill/tracking-events-generator/integration-test.js +497 -0
- package/extracted-skill/tracking-events-generator/knowledge-base.md +3066 -0
- package/extracted-skill/tracking-events-generator/micro-events.js +992 -0
- package/extracted-skill/tracking-events-generator/models/captura-de-lead.md +78 -0
- package/extracted-skill/tracking-events-generator/models/captura-lead-evento-externo.md +99 -0
- package/extracted-skill/tracking-events-generator/models/checkout-proprio.md +111 -0
- package/extracted-skill/tracking-events-generator/models/lancamento-imobiliario.md +344 -0
- package/extracted-skill/tracking-events-generator/models/multi-step-checkout.md +672 -0
- package/extracted-skill/tracking-events-generator/models/pagina-obrigado.md +55 -0
- package/extracted-skill/tracking-events-generator/models/pinterest/conversions-api-template.js +144 -0
- package/extracted-skill/tracking-events-generator/models/pinterest/event-mappings.json +48 -0
- package/extracted-skill/tracking-events-generator/models/pinterest/tag-template.js +28 -0
- package/extracted-skill/tracking-events-generator/models/quiz-funnel.md +132 -0
- package/extracted-skill/tracking-events-generator/models/reddit/conversions-api-template.js +205 -0
- package/extracted-skill/tracking-events-generator/models/reddit/event-mappings.json +56 -0
- package/extracted-skill/tracking-events-generator/models/reddit/pixel-template.js +19 -0
- package/extracted-skill/tracking-events-generator/models/scenarios/behavior-engine.js +425 -0
- package/extracted-skill/tracking-events-generator/models/scenarios/real-estate-logic.md +50 -0
- package/extracted-skill/tracking-events-generator/models/scenarios/sales-page-logic.md +50 -0
- package/extracted-skill/tracking-events-generator/models/trafego-direto.md +582 -0
- package/extracted-skill/tracking-events-generator/models/webinar-registration.md +63 -0
- package/extracted-skill/tracking-events-generator/route-intent-capture.js +222 -0
- package/extracted-skill/tracking-events-generator/tracking.config.js +46 -0
- package/extracted-skill/tracking-events-generator/walkthrough.md +26 -0
- package/package.json +89 -0
- package/scripts/build-sdk.js +106 -0
- package/server-edge-tracker/.client.env.example +14 -0
- package/server-edge-tracker/INSTALAR.md +527 -0
- package/server-edge-tracker/SEGMENTATION-DOCS.md +513 -0
- package/server-edge-tracker/config/utm-mapping.json +64 -0
- package/server-edge-tracker/deploy-client.cjs +76 -0
- package/server-edge-tracker/index.ts +1164 -0
- package/server-edge-tracker/migrate-new-db.sql +137 -0
- package/server-edge-tracker/migrate-v2.sql +16 -0
- package/server-edge-tracker/migrate-v3.sql +6 -0
- package/server-edge-tracker/migrate-v4.sql +18 -0
- package/server-edge-tracker/migrate-v5.sql +17 -0
- package/server-edge-tracker/migrate-v6.sql +24 -0
- package/server-edge-tracker/migrate-v7.sql +64 -0
- package/server-edge-tracker/migrate.sql +111 -0
- package/server-edge-tracker/modules/db.ts +702 -0
- package/server-edge-tracker/modules/dispatch/ga4.ts +72 -0
- package/server-edge-tracker/modules/dispatch/meta.ts +143 -0
- package/server-edge-tracker/modules/dispatch/platforms.ts +255 -0
- package/server-edge-tracker/modules/dispatch/tiktok.ts +107 -0
- package/server-edge-tracker/modules/dispatch/whatsapp.ts +279 -0
- package/server-edge-tracker/modules/intelligence.ts +589 -0
- package/server-edge-tracker/modules/ml/bidding.ts +247 -0
- package/server-edge-tracker/modules/ml/fraud.ts +302 -0
- package/server-edge-tracker/modules/ml/logistic.ts +226 -0
- package/server-edge-tracker/modules/ml/ltv.ts +531 -0
- package/server-edge-tracker/modules/ml/matchquality.ts +232 -0
- package/server-edge-tracker/modules/ml/quiz.ts +343 -0
- package/server-edge-tracker/modules/ml/roas.ts +255 -0
- package/server-edge-tracker/modules/ml/segmentation.ts +407 -0
- package/server-edge-tracker/modules/nurture.ts +257 -0
- package/server-edge-tracker/modules/utils.ts +311 -0
- package/server-edge-tracker/modules/utm/utm-enricher.ts +231 -0
- package/server-edge-tracker/schema-ab-ltv.sql +97 -0
- package/server-edge-tracker/schema-bidding.sql +86 -0
- package/server-edge-tracker/schema-fraud.sql +90 -0
- package/server-edge-tracker/schema-indexes.sql +67 -0
- package/server-edge-tracker/schema-ltv-feedback.sql +11 -0
- package/server-edge-tracker/schema-quiz.sql +52 -0
- package/server-edge-tracker/schema-sales-engine.sql +113 -0
- package/server-edge-tracker/schema-segmentation.sql +219 -0
- package/server-edge-tracker/schema-utm.sql +82 -0
- package/server-edge-tracker/schema.sql +265 -0
- package/server-edge-tracker/types.ts +258 -0
- package/server-edge-tracker/wrangler.toml +136 -0
- package/templates/afiliado-sem-landing.md +312 -0
- package/templates/captura-de-lead.md +78 -0
- package/templates/captura-lead-evento-externo.md +99 -0
- package/templates/checkout-proprio.md +111 -0
- package/templates/install/.claude/commands/cdp.md +1 -0
- package/templates/install/CLAUDE.md +65 -0
- package/templates/lancamento-imobiliario.md +344 -0
- package/templates/linkedin/tag-template.js +46 -0
- package/templates/multi-step-checkout.md +672 -0
- package/templates/pagina-obrigado.md +55 -0
- package/templates/pinterest/conversions-api-template.js +144 -0
- package/templates/pinterest/event-mappings.json +48 -0
- package/templates/pinterest/tag-template.js +28 -0
- package/templates/quiz-funnel.md +132 -0
- package/templates/reddit/conversions-api-template.js +205 -0
- package/templates/reddit/event-mappings.json +56 -0
- package/templates/reddit/pixel-template.js +19 -0
- package/templates/scenarios/behavior-engine.js +425 -0
- package/templates/scenarios/real-estate-logic.md +50 -0
- package/templates/scenarios/sales-page-logic.md +50 -0
- package/templates/spotify/pixel-template.js +46 -0
- package/templates/trafego-direto.md +582 -0
- package/templates/vsl-page.md +292 -0
- 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
|
+
}
|