/** * CDP Edge — index.ts (ES Module Entry Point) * * Este arquivo é o novo entry point modular do Worker. * Para usá-lo, altere em wrangler.toml: * main = "worker.js" → main = "index.ts" * * O worker.js original permanece intacto como fallback. * Todos os módulos ficam em ./modules/ */ import { ExecutionContext } from '@cloudflare/workers-types'; import { Env, TrackPayload, BehavioralData, HotmartWebhook, KiwifyWebhook, TictoWebhook } from './types'; // ── Utilitários base ────────────────────────────────────────────────────────── import { corsHeaders, sha256, META_TO_GA4, VALID_EVENT_NAMES, resolveFunnelStage, resolveIntentScore, distanceBucketWeight, computeMetaSignalWeights, metaSignalBucket, isValidEmail, sanitizeString, isValidUrl, isValidValue, isValidCurrency, isValidUTM, } from './modules/utils'; // ── Banco de dados (D1) ─────────────────────────────────────────────────────── import { saveLead, upsertProfile, resolveDeviceGraph, fireAutomation, getProfileByEmail, enrichGeoFromEdge, writeAuditLog, generateEdgeFingerprint, saveEdgeFingerprint, resurrectUTM, upsertLtvProfile, recordLtvFeedback, processWebhookDuplicateCheck, } from './modules/db'; // ── Dispatch — plataformas de ads ───────────────────────────────────────────── import { sendMetaCapi } from './modules/dispatch/meta'; import { sendGA4Mp } from './modules/dispatch/ga4'; import { sendTikTokApi } from './modules/dispatch/tiktok'; import { pushLeadToZapmanCrm } from './modules/dispatch/crm'; import { sendPinterestCapi, sendRedditCapi, sendLinkedInCapi, sendSpotifyCapi, } from './modules/dispatch/platforms'; import { sendWhatsApp, processWhatsAppWebhook, verifyHmac, } from './modules/dispatch/whatsapp'; // ── ML — LTV + A/B Testing ──────────────────────────────────────────────────── import { predictLtv, getLtvAbVariation, recordAbAssignment, handleLtvAbTestCreate, handleLtvAbTestList, handleLtvAbTestResults, handleLtvAbTestWinner, } from './modules/ml/ltv'; // ── ML — Segmentação ────────────────────────────────────────────────────────── import { handleSegmentationCluster, handleSegmentationList, handleSegmentationOutliers, handleSegmentationUpdate, } from './modules/ml/segmentation'; // ── ML — Bidding ────────────────────────────────────────────────────────────── import { handleBiddingRecommend, handleBiddingHistory, handleBiddingStatus, } from './modules/ml/bidding'; // ── ML — Fraud Detection ────────────────────────────────────────────────────── import { checkFraudGate, logFraudSignal, handleFraudAlerts, handleFraudBlocklist, handleFraudBlocklistAdd, handleFraudBlocklistRemove, handleFraudStats, } from './modules/ml/fraud'; // ── Quiz Scoring Engine (Fase 6) ────────────────────────────────────────────── import { scoreQuizAnswers, saveQuizSession, } from './modules/ml/quiz'; // ── Nurture Engine (Fase 7) ─────────────────────────────────────────────────── import { scheduleNurture } from './modules/nurture'; // ── Intelligence Agent (Cron) ───────────────────────────────────────────────── import { runIntelligenceAgent, buildGoogleCustomerMatchExport, } from './modules/intelligence'; // ── Haversine distance (km) — sem dependência externa ──────────────────────── function haversineKm(lat1: number | string | null | undefined, lon1: number | string | null | undefined, lat2: number | string | null | undefined, lon2: number | string | null | undefined): number { const R = 6371; const lat1Num = parseFloat(String(lat1 ?? '0')); const lon1Num = parseFloat(String(lon1 ?? '0')); const lat2Num = parseFloat(String(lat2 ?? '0')); const lon2Num = parseFloat(String(lon2 ?? '0')); const dLat = (lat2Num - lat1Num) * Math.PI / 180; const dLon = (lon2Num - lon1Num) * Math.PI / 180; const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1Num * Math.PI / 180) * Math.cos(lat2Num * Math.PI / 180) * Math.sin(dLon / 2) ** 2; return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); } function requireAdminAuth(request: Request, env: Env, headers: Headers): Response | null { if (!env.ADMIN_API_TOKEN) { return new Response(JSON.stringify({ error: 'ADMIN_API_TOKEN não configurado' }), { status: 503, headers }); } const authHeader = request.headers.get('Authorization') || ''; const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : ''; if (!token || token !== env.ADMIN_API_TOKEN) { return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401, headers }); } return null; } async function buildHealthReport(env: Env) { const results: Record = {}; try { await env.DB?.prepare('SELECT 1').run(); results.d1 = 'ok'; } catch (err: any) { results.d1 = `FAILED: ${err?.message || String(err)}`; } try { await env.GEO_CACHE?.get('__health_check__'); results.kv = 'ok'; } catch (err: any) { results.kv = `FAILED: ${err?.message || String(err)}`; } try { await env.AI?.run('@cf/ibm-granite/granite-4.0-h-micro', { messages: [{ role: 'user', content: 'ping' }], max_tokens: 1, }); results.ai = 'ok'; } catch (err: any) { results.ai = `FAILED: ${err?.message || String(err)}`; } const vars = { META_PIXEL_ID: env.META_PIXEL_ID ? 'set' : 'MISSING', GA4_MEASUREMENT_ID: env.GA4_MEASUREMENT_ID ? 'set' : 'MISSING', TIKTOK_PIXEL_ID: env.TIKTOK_PIXEL_ID ? 'set' : 'MISSING', SITE_DOMAIN: env.SITE_DOMAIN ? 'set' : 'MISSING', }; const secrets = { META_ACCESS_TOKEN: env.META_ACCESS_TOKEN ? 'set' : 'MISSING', GA4_API_SECRET: env.GA4_API_SECRET ? 'set' : 'MISSING', WA_WEBHOOK_VERIFY_TOKEN: env.WA_WEBHOOK_VERIFY_TOKEN ? 'set' : 'MISSING', WHATSAPP_ACCESS_TOKEN: (env.WHATSAPP_ACCESS_TOKEN || env.WA_ACCESS_TOKEN) ? 'set' : 'not set (optional - only for auto-reply)', WHATSAPP_PHONE_NUMBER_ID: (env.WHATSAPP_PHONE_NUMBER_ID || env.WA_PHONE_ID) ? 'set' : 'not set (optional - only for auto-reply)', WA_NOTIFY_NUMBER: env.WA_NOTIFY_NUMBER ? 'set' : 'not set (optional - only for auto-reply)', TIKTOK_ACCESS_TOKEN: env.TIKTOK_ACCESS_TOKEN ? 'set' : 'not set (optional)', CALLMEBOT_PHONE: env.CALLMEBOT_PHONE ? 'set' : 'not set (optional)', ZAPMAN_API_URL: env.ZAPMAN_API_URL ? 'set' : 'not set (optional - ZapMan SDR)', ZAPMAN_API_KEY: env.ZAPMAN_API_KEY ? 'set' : 'not set (optional - ZapMan SDR)', ZAPMAN_WEBHOOK_URL: env.ZAPMAN_WEBHOOK_URL ? 'set' : 'not set (optional - ZapMan SDR)', }; const hasMissing = Object.values(vars).includes('MISSING') || Object.values(secrets).includes('MISSING') || results.d1 !== 'ok'; return { status: hasMissing ? 'degraded' : 'ok', timestamp: new Date().toISOString(), bindings: results, vars, secrets, }; } // ───────────────────────────────────────────────────────────────────────────── // HANDLER PRINCIPAL // ───────────────────────────────────────────────────────────────────────────── export default { async fetch(request: Request, env: Env, ctx: ExecutionContext) { const origin = request.headers.get('Origin') || ''; const headersObj = { 'Content-Type': 'application/json', ...corsHeaders(origin, env.SITE_DOMAIN || null), }; const headers = new Headers(headersObj); // Preflight CORS if (request.method === 'OPTIONS') { return new Response(null, { status: 204, headers }); } const url = new URL(request.url); // ── Rate Limiter — camada 0, antes do Fraud Gate ───────────────────────── // Usa apenas CF-Connecting-IP (injetado pela Cloudflare, não pode ser spoofado) // X-Forwarded-For pode ser falsificado por atacantes para bypass rate limiter if (url.pathname === '/track' && request.method === 'POST' && env.RATE_LIMITER) { const ip = request.headers.get('CF-Connecting-IP') || 'unknown'; const { success } = await env.RATE_LIMITER.limit({ key: ip }); if (!success) { return new Response(JSON.stringify({ status: 'ok', queued: true }), { status: 200, headers }); } } // ── Fraud Gate — Fase 4 (apenas em /track) ──────────────────────────────── // Roda ANTES de qualquer processamento de evento // Silent drop (200) — bots não sabem que foram detectados if (url.pathname === '/track' && request.method === 'POST') { let trackBodyForFraud: TrackPayload = {}; try { const cloned = request.clone(); trackBodyForFraud = await cloned.json().catch(() => ({})) as TrackPayload; } catch { trackBodyForFraud = {}; } const earlyEventId = String(trackBodyForFraud.eventId || trackBodyForFraud.event_id || '').trim(); if (env.DB && earlyEventId) { try { const existingEvent = await env.DB .prepare('SELECT event_id FROM events WHERE event_id = ? LIMIT 1') .bind(earlyEventId) .first(); const existingLead = await env.DB .prepare('SELECT id FROM leads WHERE event_id = ? LIMIT 1') .bind(earlyEventId) .first(); if (existingEvent || existingLead) { return new Response(JSON.stringify({ ok: true, skipped: 'duplicate' }), { status: 200, headers }); } } catch { // Dedup early-check falhou; segue para o Fraud Gate fail-safe. } } const fraudResult = await checkFraudGate(env, request, trackBodyForFraud); if (!fraudResult.allowed) { ctx.waitUntil(logFraudSignal(env, request, trackBodyForFraud, fraudResult)); return new Response(JSON.stringify({ status: 'ok', queued: true }), { status: 200, headers }); } if (fraudResult.action === 'flagged') { ctx.waitUntil(logFraudSignal(env, request, trackBodyForFraud, fraudResult)); } } // ── GET /export/customer-match ──────────────────────────────────────────── if (request.method === 'GET' && url.pathname === '/export/customer-match') { const authHeader = request.headers.get('Authorization') || ''; const token = authHeader.replace('Bearer ', ''); if (!env.META_ACCESS_TOKEN || token !== env.META_ACCESS_TOKEN) { return new Response('Unauthorized', { status: 401 }); } const rows = await buildGoogleCustomerMatchExport(env); return new Response(JSON.stringify({ total: rows.length, data: rows }, null, 2), { headers: { ...headers, 'Content-Disposition': 'attachment; filename="customer-match.json"' }, }); } // ── GET /health ─────────────────────────────────────────────────────────── if (request.method === 'GET' && url.pathname === '/health') { return new Response(JSON.stringify({ status: 'ok', timestamp: new Date().toISOString(), }, null, 2), { headers }); } // ── GET /validate-install ───────────────────────────────────────────────── // Endpoint de diagnóstico pós-deploy. Chamado por `cdp-edge validate `. // Testa D1 write/read, KV, AI e retorna relatório estruturado. // Protegido: exige Authorization: Bearer if (request.method === 'GET' && url.pathname === '/validate-install') { const authError = requireAdminAuth(request, env, headers); if (authError) return authError; const report: Record = {}; // 1. D1 write + read try { const testId = `__cdp_validate_${Date.now()}__`; await env.DB?.prepare( `INSERT OR REPLACE INTO events (event_id, event_name, user_id, created_at) VALUES (?, '__validate__', '__validate__', datetime('now'))` ).bind(testId).run(); const row = await env.DB?.prepare( `SELECT event_id FROM events WHERE event_id = ?` ).bind(testId).first(); await env.DB?.prepare(`DELETE FROM events WHERE event_id = ?`).bind(testId).run(); report.d1 = { ok: !!row, detail: row ? 'write+read+delete OK' : 'row not found after insert' }; } catch (err: any) { report.d1 = { ok: false, detail: err?.message || String(err) }; } // 2. KV read/write try { await env.GEO_CACHE?.put('__cdp_validate__', '1', { expirationTtl: 60 }); const val = await env.GEO_CACHE?.get('__cdp_validate__'); report.kv = { ok: val === '1', detail: val === '1' ? 'write+read OK' : 'value mismatch' }; } catch (err: any) { report.kv = { ok: false, detail: err?.message || String(err) }; } // 3. Workers AI try { await env.AI?.run('@cf/ibm-granite/granite-4.0-h-micro', { messages: [{ role: 'user', content: 'ping' }], max_tokens: 1, }); report.ai = { ok: true, detail: 'Granite 4.0 Micro respondeu' }; } catch (err: any) { report.ai = { ok: false, detail: err?.message || String(err) }; } // 4. Secrets críticos const missing: string[] = []; if (!env.META_PIXEL_ID) missing.push('META_PIXEL_ID'); if (!env.META_ACCESS_TOKEN) missing.push('META_ACCESS_TOKEN'); if (!env.SITE_DOMAIN) missing.push('SITE_DOMAIN'); report.secrets = { ok: missing.length === 0, detail: missing.length === 0 ? 'todos os secrets críticos configurados' : `MISSING: ${missing.join(', ')}`, }; // 5. /track endpoint (auto-teste) const trackTest = { ok: false, detail: '' }; try { const testPayload = { eventName: 'PageView', userId: '__cdp_validate__', pageUrl: `https://${env.SITE_DOMAIN || 'validate.test'}/`, userAgent: request.headers.get('User-Agent') || '', ip: request.headers.get('CF-Connecting-IP') || '', _validate: true, }; // Não chama fetch externo — apenas verifica que o payload seria aceito const hasRequired = testPayload.eventName && testPayload.userId; trackTest.ok = !!hasRequired; trackTest.detail = hasRequired ? 'payload de teste válido (eventName + userId presentes)' : 'payload inválido'; } catch (err: any) { trackTest.detail = err?.message || String(err); } report.track_endpoint = trackTest; const allOk = Object.values(report).every(r => r.ok); return new Response(JSON.stringify({ status: allOk ? 'ok' : 'degraded', timestamp: new Date().toISOString(), checks: report, }, null, 2), { status: allOk ? 200 : 207, headers, }); } // ── POST /track ─────────────────────────────────────────────────────────── if (request.method === 'POST' && url.pathname === '/track') { // Reject oversized payloads before reading body (64 KB limit) const contentLength = parseInt(request.headers.get('Content-Length') || '0', 10); if (contentLength > 65536) { return new Response(JSON.stringify({ error: 'Payload muito grande' }), { status: 413, headers }); } let body; try { body = await request.json(); } catch { return new Response(JSON.stringify({ error: 'JSON inválido' }), { status: 400, headers }); } if (typeof body !== 'object' || Array.isArray(body) || body === null) { return new Response(JSON.stringify({ error: 'Payload inválido' }), { status: 400, headers }); } const STR_FIELDS = ['email','phone','firstName','lastName','city','state','zip','userId', 'utmSource','utmMedium','utmCampaign','utmContent','utmTerm', 'fbclid','ttclid','gclid','transactionId','productName','currency']; const { eventName: _bodyEventName, behavioral_data, ...payload } = body as { eventName?: string; behavioral_data?: BehavioralData; [key: string]: any }; const trackPayload: TrackPayload = payload; // Aceita eventName (camelCase) ou event_name (snake_case — formato cdpTrack.js SDK) const eventName = _bodyEventName || (payload.event_name as string | undefined); // ── Extrair click_ids e utms de sub-objetos (formato cdpTrack.js) ──────── // cdpTrack.js envia fbp/fbc/fbclid dentro de click_ids{} e UTMs dentro de utms{} // O Worker trabalha com campos no nível raiz — esta extração resolve o mismatch. if (payload.click_ids && typeof payload.click_ids === 'object') { const c = payload.click_ids as Record; if (!trackPayload.fbp && c.fbp) trackPayload.fbp = c.fbp; if (!trackPayload.fbc && c.fbc) trackPayload.fbc = c.fbc; if (!trackPayload.fbclid && c.fbclid) trackPayload.fbclid = c.fbclid; if (!trackPayload.gclid && c.gclid) trackPayload.gclid = c.gclid; if (!trackPayload.wbraid && c.wbraid) trackPayload.wbraid = c.wbraid; if (!trackPayload.gbraid && c.gbraid) trackPayload.gbraid = c.gbraid; if (!trackPayload.ttclid && c.ttclid) trackPayload.ttclid = c.ttclid; if (!trackPayload.ttp && c.ttp) trackPayload.ttp = c.ttp; if (!trackPayload.msclkid && c.msclkid) trackPayload.msclkid = c.msclkid; } if (payload.utms && typeof payload.utms === 'object') { const u = payload.utms as Record; if (!trackPayload.utmSource && u.utm_source) trackPayload.utmSource = u.utm_source; if (!trackPayload.utmMedium && u.utm_medium) trackPayload.utmMedium = u.utm_medium; if (!trackPayload.utmCampaign && u.utm_campaign) trackPayload.utmCampaign = u.utm_campaign; if (!trackPayload.utmContent && u.utm_content) trackPayload.utmContent = u.utm_content; if (!trackPayload.utmTerm && u.utm_term) trackPayload.utmTerm = u.utm_term; } // ── Normalizar campos snake_case → camelCase (formato cdpTrack.js SDK) ── if (!trackPayload.userId && payload.user_id) trackPayload.userId = payload.user_id; if (!trackPayload.eventId && payload.event_id) trackPayload.eventId = payload.event_id; if (!trackPayload.pageUrl && payload.page_url) trackPayload.pageUrl = payload.page_url; if (!trackPayload.sessionId && payload.session_id) trackPayload.sessionId = payload.session_id; // ── Validação de eventName ──────────────────────────────────────── if (!eventName) { return new Response(JSON.stringify({ error: 'eventName é obrigatório' }), { status: 400, headers }); } if (typeof eventName !== 'string' || eventName.length > 64 || !VALID_EVENT_NAMES.has(eventName)) { return new Response(JSON.stringify({ error: `eventName desconhecido: ${eventName.slice(0, 64)}` }), { status: 400, headers }); } // ── Sanitização e Validação de Campos String ────────────────────── type SanitizeResult = { error?: string; sanitized: string | null }; const SANITIZE_FIELDS: Record SanitizeResult> = { email: (val: string) => { if (!isValidEmail(val)) return { error: 'email inválido (formato incorreto)', sanitized: null }; return { sanitized: val.toLowerCase().trim() }; }, firstName: (val: string) => ({ sanitized: sanitizeString(val, 100) }), lastName: (val: string) => ({ sanitized: sanitizeString(val, 100) }), city: (val: string) => ({ sanitized: sanitizeString(val, 100) }), state: (val: string) => ({ sanitized: sanitizeString(val, 100) }), zip: (val: string) => ({ sanitized: sanitizeString(val, 20) }), dob: (val: string) => ({ sanitized: sanitizeString(val, 20) }), productName: (val: string) => ({ sanitized: sanitizeString(val, 200) }), pageUrl: (val: string) => { if (!isValidUrl(val)) return { error: 'pageUrl inválido (formato incorreto)', sanitized: null }; return { sanitized: val.trim() }; }, currency: (val: string) => { if (!isValidCurrency(val)) return { error: 'currency inválido (deve ser código ISO 4217)', sanitized: null }; return { sanitized: val.trim().toUpperCase() }; }, }; // Sanitiza e valida campos específicos for (const [field, validator] of Object.entries(SANITIZE_FIELDS)) { const value = trackPayload[field as keyof TrackPayload]; if (value !== undefined && value !== null) { if (typeof value !== 'string') { return new Response(JSON.stringify({ error: `Campo ${field} deve ser string` }), { status: 400, headers }); } const result = validator(value); if (result.error) { return new Response(JSON.stringify({ error: result.error }), { status: 400, headers }); } if (result.sanitized !== null) { trackPayload[field as keyof TrackPayload] = result.sanitized as any; } } } // Sanitiza campos de string genéricos const GENERIC_SANITIZE_FIELDS = ['utmSource', 'utmMedium', 'utmCampaign', 'utmContent', 'utmTerm']; for (const field of GENERIC_SANITIZE_FIELDS) { const value = trackPayload[field as keyof TrackPayload]; if (value !== undefined && value !== null) { if (typeof value !== 'string') { return new Response(JSON.stringify({ error: `Campo ${field} deve ser string` }), { status: 400, headers }); } const utmType = `utm_${field.replace('utm', '').toLowerCase()}`; if (!isValidUTM(value, utmType)) { return new Response(JSON.stringify({ error: `Campo ${field} contém caracteres perigosos` }), { status: 400, headers }); } const sanitized = sanitizeString(value, 200); if (sanitized === null) { return new Response(JSON.stringify({ error: `Campo ${field} inválido após sanitização` }), { status: 400, headers }); } trackPayload[field as keyof TrackPayload] = sanitized as any; } } // Sanitiza campos de string restantes const TRACKING_ID_FIELDS = ['transactionId', 'fbclid', 'ttclid', 'gclid']; for (const field of TRACKING_ID_FIELDS) { const value = trackPayload[field as keyof TrackPayload]; if (value !== undefined && value !== null) { if (typeof value !== 'string') { return new Response(JSON.stringify({ error: `Campo ${field} deve ser string` }), { status: 400, headers }); } const sanitized = sanitizeString(value, 512); if (sanitized === null) { return new Response(JSON.stringify({ error: `Campo ${field} inválido após sanitização` }), { status: 400, headers }); } trackPayload[field as keyof TrackPayload] = sanitized as any; } } // ── fbc derivado de fbclid ─────────────────────────────────────────── // Quando o usuário vem de um clique Meta, fbclid chega na URL e o cdpTrack.js // o envia como campo. O CAPI espera fbc no formato fb.1.{ms}.{fbclid}. // Sem essa conversão, o sinal de clique some — EMQ cai e deduplicação piora. if (trackPayload.fbclid && !trackPayload.fbc) { trackPayload.fbc = `fb.1.${Date.now()}.${trackPayload.fbclid}`; } // ── Validação de Valor Numérico ─────────────────────────────────── if (trackPayload.value !== undefined && trackPayload.value !== null) { if (!isValidValue(trackPayload.value)) { return new Response(JSON.stringify({ error: 'value fora do intervalo permitido (0-9,999,999)' }), { status: 400, headers }); } trackPayload.value = Number(trackPayload.value); } // ── Extrair dados comportamentais do browser ────────────────────────── if (behavioral_data) { payload.engagementScore = behavioral_data.engagement_score ?? behavioral_data.totalScore ?? null; payload.intentionLevel = behavioral_data.intention_level ?? null; payload.userScore = behavioral_data.user_score ?? null; // Sinais de engajamento profundo — usados no anti-falso-positivo e no LTV payload.scrollScore = behavioral_data.scroll_score ?? null; payload.timeLevel = behavioral_data.time_level ?? null; // ── Sanitiza dados do behavioral_data ──────────────────────── // Os dados do behavioral_data podem vir do browser e ser manipulados const sanitizedBehavioralEmail = behavioral_data.email && isValidEmail(behavioral_data.email) ? behavioral_data.email.toLowerCase().trim() : null; const sanitizedBehavioralPhone = behavioral_data.phone ? sanitizeString(behavioral_data.phone, 50) : null; const sanitizedBehavioralFirstName = behavioral_data.first_name || behavioral_data.firstName ? sanitizeString(behavioral_data.first_name || behavioral_data.firstName, 100) : null; const sanitizedBehavioralLastName = behavioral_data.last_name || behavioral_data.lastName ? sanitizeString(behavioral_data.last_name || behavioral_data.lastName, 100) : null; const sanitizedBehavioralCity = behavioral_data.city ? sanitizeString(behavioral_data.city, 100) : null; // Usa dados sanitizados do behavioral_data se não existirem no payload principal payload.email = payload.email || sanitizedBehavioralEmail; payload.phone = payload.phone || sanitizedBehavioralPhone; payload.firstName = payload.firstName || sanitizedBehavioralFirstName; payload.lastName = payload.lastName || sanitizedBehavioralLastName; payload.city = payload.city || sanitizedBehavioralCity; // Sanitiza campos restantes do behavioral_data const sanitizedBehavioralState = behavioral_data.state ? sanitizeString(behavioral_data.state, 100) : null; const sanitizedBehavioralZip = behavioral_data.zip ? sanitizeString(behavioral_data.zip, 20) : null; const sanitizedBehavioralDob = behavioral_data.dob ? sanitizeString(behavioral_data.dob, 20) : null; payload.state = payload.state || sanitizedBehavioralState; payload.zip = payload.zip || sanitizedBehavioralZip; payload.dob = payload.dob || sanitizedBehavioralDob; } // ── Normalização de intent_score → 0.0–1.0 ────────────────────────── // Aceita string ('high'/'medium'/'low'), 0-1 ou 0-100 enviados pelo front. // intent_bucket mantém a label legível para D1 e logs. const intentScoreNum = resolveIntentScore(payload.intent_score); if (intentScoreNum !== null) { payload.intent_score = intentScoreNum; payload.intentScoreNum = intentScoreNum; payload.intent_bucket = intentScoreNum >= 0.8 ? 'high' : intentScoreNum >= 0.5 ? 'medium' : 'low'; } else { payload.intentScoreNum = null; } // ── Anti-falso-positivo ─────────────────────────────────────────────── // Penaliza intent se engajamento insuficiente: scroll raso E tempo curto. // scroll_score < 2.0 ≈ não passou de 50% da página. // time_level 'curioso' = menos de 60 segundos na página. if (payload.intentScoreNum !== null) { const isShallowScroll = payload.scrollScore !== null && payload.scrollScore < 2.0; const isShallowTime = payload.timeLevel === 'curioso'; if (isShallowScroll && isShallowTime) { const penalized = Math.round(payload.intentScoreNum * 0.7 * 100) / 100; payload.intentScoreNum = penalized; payload.intent_score = penalized; payload.intent_bucket = penalized >= 0.8 ? 'high' : penalized >= 0.5 ? 'medium' : 'low'; payload.intent_penalized = true; // flag auditável — visível no D1 e logs } } // ── Edge Fingerprint + UTM Resurrection ────────────────────────────── const fingerprint = await generateEdgeFingerprint(request); payload.utmRestored = false; if (fingerprint && env.DB) { if (payload.utmSource) { ctx.waitUntil(saveEdgeFingerprint(env.DB, fingerprint, payload.userId, payload)); } else { const recovered = await resurrectUTM(env.DB, fingerprint); if (recovered) { payload.utmSource = payload.utmSource || recovered.utm_source; payload.utmMedium = payload.utmMedium || recovered.utm_medium; payload.utmCampaign = payload.utmCampaign || recovered.utm_campaign; payload.utmContent = payload.utmContent || recovered.utm_content; payload.utmTerm = payload.utmTerm || recovered.utm_term; payload.utmRestored = true; } } } // ── Bot Mitigation ──────────────────────────────────────────────────── const botScoreStr = (request as any).cf?.botManagement?.score; const cfBotScore = botScoreStr !== undefined ? parseInt(String(botScoreStr)) : 100; const ua = (request.headers.get('User-Agent') || '').toLowerCase(); const isBotPattern = /bot|crawler|spider|lighthouse|postman|curl|inspect|headless/i.test(ua); const isBot = cfBotScore < 30 || isBotPattern; trackPayload.botScore = isBot ? 2 : (cfBotScore < 60 ? 1 : 0); if (isBot && !['Contact', 'Lead', 'Purchase'].includes(eventName)) { return new Response(JSON.stringify({ ok: true, skipped_due_to_bot: true }), { status: 200, headers }); } // ── Edge Geo Enrichment ─────────────────────────────────────────────── const geoData = await enrichGeoFromEdge(request, env, payload); // ── First-Party Cookie (Identity Resolution) ────────────────────────── const cookieHeader = request.headers.get('Cookie') || ''; const cdpUidMatch = cookieHeader.match(/_cdp_uid=([^;]+)/); const finalUserId = payload.userId || (cdpUidMatch ? cdpUidMatch[1] : crypto.randomUUID()); payload.userId = finalUserId; // Deduplica antes de enrichments caros e dispatch externo. const dedupEventId = String(payload.eventId || payload.event_id || '').trim(); if (env.DB && dedupEventId) { try { const existingEvent = await env.DB .prepare('SELECT event_id FROM events WHERE event_id = ? LIMIT 1') .bind(dedupEventId) .first(); const existingLead = await env.DB .prepare('SELECT id FROM leads WHERE event_id = ? LIMIT 1') .bind(dedupEventId) .first(); if (existingEvent || existingLead) { return new Response(JSON.stringify({ ok: true, skipped: 'duplicate' }), { status: 200, headers }); } await env.DB.prepare( `INSERT INTO events (event_id, event_name, user_id, created_at) VALUES (?, ?, ?, datetime('now'))` ).bind(dedupEventId, eventName, payload.userId || null).run(); } catch { // Tabela ausente ou erro de DB não bloqueia tracking. } } const ga4Name = META_TO_GA4[eventName] || eventName.toLowerCase(); // ── Dual-layer semantics ───────────────────────────────────────────── // Meta sempre recebe o nome canônico (Schedule, Lead, etc.). // Internamente o CDP usa semântica de funil precisa via FUNNEL_TAXONOMY. if (payload.funnel_stage) { const { depth, funnelDepth } = resolveFunnelStage(payload.funnel_stage); payload.funnelDepth = funnelDepth; // ex: 'bottom_intent', 'bottom_conversion' payload.funnelLevel = depth; // ex: 'bottom', 'conversion', 'mid' } if (eventName === 'Schedule' && payload.funnel_stage === 'route_click') { payload.internalEvent = 'IntentToVisit'; } // ── Real Estate Distance Enrichment ────────────────────────────────── // Calcula distância entre o IP do usuário (via Cloudflare) e o imóvel. // Ativa quando o evento carrega property_lat + property_lng (ex: Schedule de rota). const propLat = parseFloat(String(trackPayload.property_lat ?? trackPayload.propertyLat)); const propLng = parseFloat(String(trackPayload.property_lng ?? trackPayload.propertyLng)); const userLat = parseFloat(String(request.cf?.latitude ?? '0')); const userLng = parseFloat(String(request.cf?.longitude ?? '0')); if (!isNaN(propLat) && !isNaN(propLng) && !isNaN(userLat) && !isNaN(userLng)) { const distKm = haversineKm(userLat, userLng, propLat, propLng); trackPayload.distanceKm = Math.round(distKm * 10) / 10; trackPayload.distanceBucket = distKm < 5 ? 'very_close' : distKm < 15 ? 'close' : distKm < 30 ? 'nearby' : distKm < 60 ? 'moderate' : 'far'; } // ── Quiz Scoring Engine (Fase 6) ───────────────────────────────────── // Roda antes do LTV para que intentionLevel qualificado alimente a predição. // O front envia quiz_answers: [{question, answer, step}] junto com QuizComplete. if (eventName === 'QuizComplete' && Array.isArray(payload.quiz_answers) && payload.quiz_answers.length > 0) { try { const quizResult = await scoreQuizAnswers(env, payload.quiz_answers, payload.quiz_name ?? null); // Injeta qualificação no payload — flui para LTV, Meta Signal, D1 e CAPI payload.intentionLevel = quizResult.qualification; payload.intent_score = quizResult.intent_score; payload.intentScoreNum = quizResult.intent_score; payload.intent_bucket = quizResult.intent_score >= 0.8 ? 'high' : quizResult.intent_score >= 0.5 ? 'medium' : 'low'; // Campos extras para auditoria e dashboard (payload as any).quiz_qualification = quizResult.qualification; (payload as any).quiz_confidence = quizResult.confidence; (payload as any).quiz_weighted_score = quizResult.weighted_score; (payload as any).quiz_dominant_dimension = quizResult.dominant_dimension; (payload as any).quiz_signals = quizResult.dimensions.map((d: any) => d.signal).filter(Boolean); (payload as any).quiz_source = quizResult.source; // utm_term — injetado pelo Worker após qualificação (nunca configurado nos anúncios) // Flui para Meta CAPI custom_data e D1 user_profiles para cruzar com ROAS Feedback payload.utmTerm = quizResult.qualification; // comprador | interessado | curioso | perdido // Persiste sessão no D1 em background if (env.DB) ctx.waitUntil(saveQuizSession(env, payload.userId, payload, quizResult)); // Agenda nurture sequence baseada na qualificação (background) ctx.waitUntil(scheduleNurture(env, payload, quizResult.qualification)); } catch (err: any) { console.error('[QuizScoring] erro ao qualificar respostas:', err?.message || String(err)); // Fail-safe: continua sem qualificação } } // ── LTV Prediction (+ A/B Testing de Prompts) ───────────────────────── const LTV_EVENTS = ['Lead', 'Contact', 'Schedule', 'CompleteRegistration', 'QuizComplete']; if (LTV_EVENTS.includes(eventName) && !payload.value) { const abVariation = await getLtvAbVariation(env); const ltv = await predictLtv(env, payload, request, abVariation?.system_prompt || null); payload.value = ltv.value; payload.currency = payload.currency || 'BRL'; payload.ltvClass = ltv.class; payload.ltvScore = ltv.score; ctx.waitUntil(upsertLtvProfile(env, payload.userId, ltv)); if (abVariation) { const emailHash = payload.email ? await sha256(payload.email.trim().toLowerCase()) : null; ctx.waitUntil( recordAbAssignment( env, payload.userId, abVariation.id, abVariation.test_id, ltv.value, ltv.class, emailHash ?? null, ) ); } } // ── LTV Feedback Loop — fecha o ciclo preditivo ────────────────────── // Quando uma compra real acontece, registra o valor real e recalcula accuracy. // Alimenta ltv_ab_variations.accuracy_score → autoDecideAbWinner usa isso. if (eventName === 'Purchase' && payload.value > 0) { ctx.waitUntil(recordLtvFeedback(env, payload.userId, payload.value)); } // ── Meta Signal Score (composite, pesos dinâmicos) ─────────────────── // Pesos variam por profundidade de funil: fundo = comportamento pesa mais. { const w = computeMetaSignalWeights(payload.funnelLevel); const iW = payload.intentScoreNum ?? 0.5; const lW = payload.ltvScore ? payload.ltvScore / 100 : 0.5; const dW = distanceBucketWeight(payload.distanceBucket); payload.metaSignal = Math.round(((iW * w.intent) + (lW * w.ltv) + (dW * w.dist)) * 100) / 100; payload.metaSignalBucket = metaSignalBucket(payload.metaSignal); // 'hot'/'warm'/'cold' } // ── Hot Lead Trigger (timing + sinal) ──────────────────────────────── // Reage em tempo real: WhatsApp apenas quando comportamento + sinal justificam. // Critérios: rota + muito próximo + (intent alto OU meta_signal alto) // + (janela de decisão BRT 18–22h OU sinal excepcional ≥ 0.9) const hourBRT = (new Date().getUTCHours() - 3 + 24) % 24; const inWindow = hourBRT >= 18 && hourBRT <= 22; const isHotLead = payload.funnel_stage === 'route_click' && payload.distanceBucket === 'very_close' && ((payload.intentScoreNum ?? 0) >= 0.8 || payload.metaSignal >= 0.85) && (inWindow || payload.metaSignal >= 0.9); // Cross-Device Graph — background if (env.DB && payload.userId && (payload.email || payload.phone)) { ctx.waitUntil(resolveDeviceGraph(env.DB, payload.userId, payload.email, payload.phone)); } // R2 Audit Log — background ctx.waitUntil(writeAuditLog(env, eventName, payload, geoData)); // Disparar tudo em paralelo const WHATSAPP_NOTIFY_EVENTS = ['Lead', 'Purchase', 'CompleteRegistration']; const [metaRes, ga4Res, ttRes] = await Promise.allSettled([ sendMetaCapi(env, eventName, payload, request, ctx), sendGA4Mp(env, ga4Name, payload, ctx), sendTikTokApi(env, eventName, payload, request, ctx), saveLead(env, payload.internalEvent || eventName, payload, request, 'website'), upsertProfile(env, eventName, payload, request), ...(WHATSAPP_NOTIFY_EVENTS.includes(eventName) || isHotLead ? [sendWhatsApp(env, isHotLead ? 'hot_lead_intent_to_visit' : eventName, payload)] : []), ]); // ZapMan CRM — push automático quando Lead ou Contact if (['Lead', 'Contact'].includes(eventName) && payload.phone) { const phoneNorm = String(payload.phone).replace(/\D/g, ''); const e164 = phoneNorm.startsWith('55') ? phoneNorm : `55${phoneNorm}`; ctx.waitUntil( pushLeadToZapmanCrm(env, { phone: e164, name: payload.firstName ? `${payload.firstName} ${payload.lastName || ''}`.trim() : undefined, email: payload.email || '', empresa: payload.company || '', campanha: payload.utmCampaign || payload.utm_campaign || '', origem: 'meta_api', }) ); } // Automação de mensagens const AUTOMATION_EVENTS = ['Lead', 'Purchase', 'InitiateCheckout']; if (AUTOMATION_EVENTS.includes(eventName) && env.DB) { const db = env.DB; // Captura em variável local ctx.waitUntil( (async () => { try { const lastLead = await db .prepare(`SELECT id FROM leads WHERE event_id = ?1 LIMIT 1`) .bind(trackPayload.eventId || trackPayload.event_id || '') .first() as any; const leadId = lastLead?.id ? Number(lastLead.id) : null; if (leadId) await fireAutomation(env, eventName, leadId, trackPayload); } catch (e: any) { console.error('[Automation] lead lookup error:', e?.message || String(e)); } })() ); } // Edge Personalization let currentScore = 0; if (env.DB && trackPayload.userId) { try { const profileRow = await env.DB.prepare('SELECT score FROM user_profiles WHERE user_id = ?').bind(trackPayload.userId).first(); if (profileRow) currentScore = Number(profileRow.score) || 0; } catch (err: any) { console.error('[POST /track] Error fetching user profile score:', { userId: trackPayload.userId, error: err?.message || String(err), stack: err?.stack, }); } } const resHeaders = new Headers(headers); resHeaders.set('Set-Cookie', `_cdp_uid=${finalUserId}; HttpOnly; Secure; SameSite=Lax; Max-Age=31536000; Path=/; Domain=.${env.SITE_DOMAIN}`); return new Response(JSON.stringify({ ok: true, userProfile: { score: currentScore, user_id: finalUserId }, meta: metaRes.status === 'fulfilled' ? metaRes.value : { error: metaRes.reason?.message }, ga4: ga4Res.status === 'fulfilled' ? ga4Res.value : { error: ga4Res.reason?.message }, tiktok: ttRes.status === 'fulfilled' ? ttRes.value : { error: ttRes.reason?.message }, }), { status: 200, headers: resHeaders }); } // ── POST /webhook/hotmart ───────────────────────────────────────────────── if (request.method === 'POST' && url.pathname === '/webhook/hotmart') { if (env.WEBHOOK_SECRET_HOTMART) { const token = request.headers.get('X-Hotmart-Webhook-Token') || ''; if (token !== env.WEBHOOK_SECRET_HOTMART) { return new Response('Unauthorized', { status: 401 }); } } let wh: HotmartWebhook; try { wh = await request.json() as HotmartWebhook; } catch { return new Response('JSON inválido', { status: 400 }); } const data = wh.data || wh; const buyer = data.buyer || {}; const purchase = data.purchase || {}; const product = data.product || {}; if (!['APPROVED', 'COMPLETE', 'approved', 'complete'].includes(purchase.status)) { return new Response(JSON.stringify({ skipped: `status ${purchase.status}` }), { status: 200, headers }); } const hmTxId = String(purchase.transaction || ''); const dupCheck = await processWebhookDuplicateCheck(env, 'hotmart', hmTxId, JSON.stringify(wh), { email: buyer.email, }); if (dupCheck.duplicate) { return new Response(JSON.stringify({ ok: true, skipped: 'duplicate' }), { status: 200, headers }); } const profile = await getProfileByEmail(env, buyer.email); const payload = { email: buyer.email, phone: buyer.phone, firstName: buyer.name?.split(' ')[0], lastName: buyer.name?.split(' ').slice(1).join(' ') || undefined, fbp: profile?.fbp, fbc: profile?.fbc, userId: profile?.user_id, gaClientId: profile?.ga_client_id, value: purchase.price?.value, currency: purchase.price?.currency_value || 'BRL', contentIds: [String(product.id || product.ucode || '')], contentName: product.name, contentType: 'product', pageUrl: profile?.page_url || `https://${env.SITE_DOMAIN || 'seu-dominio.com.br'}`, orderId: purchase.transaction, eventId: `hotmart_${purchase.transaction}`, city: profile?.city, state: profile?.state, country: profile?.country, }; ctx.waitUntil(Promise.allSettled([ sendMetaCapi(env, 'Purchase', payload, request, ctx), sendGA4Mp(env, 'purchase', payload, ctx), sendTikTokApi(env, 'CompletePayment', payload, request, ctx), saveLead(env, 'Purchase', payload, request, 'hotmart'), sendWhatsApp(env, 'Purchase', payload), ])); return new Response(JSON.stringify({ ok: true }), { status: 200, headers }); } // ── POST /webhook/kiwify ────────────────────────────────────────────────── if (request.method === 'POST' && url.pathname === '/webhook/kiwify') { if (env.WEBHOOK_SECRET_KIWIFY) { const token = request.headers.get('X-Kiwify-Event-Token') || ''; if (token !== env.WEBHOOK_SECRET_KIWIFY) { return new Response('Unauthorized', { status: 401 }); } } let wh: KiwifyWebhook; try { wh = await request.json() as KiwifyWebhook; } catch { return new Response('JSON inválido', { status: 400 }); } if (wh.order_status !== 'paid' && wh.order_status !== 'approved') { return new Response(JSON.stringify({ skipped: `status ${wh.order_status}` }), { status: 200, headers }); } const customer = wh.Customer || {}; const kwTxId = String(wh.order_id || ''); const dupCheck = await processWebhookDuplicateCheck(env, 'kiwify', kwTxId, JSON.stringify(wh), { email: customer.email, }); if (dupCheck.duplicate) { return new Response(JSON.stringify({ ok: true, skipped: 'duplicate' }), { status: 200, headers }); } const product = wh.Product || {}; const profile = await getProfileByEmail(env, customer.email || ''); const payload = { email: customer.email, phone: customer.mobile, firstName: customer.full_name?.split(' ')[0], lastName: customer.full_name?.split(' ').slice(1).join(' ') || undefined, fbp: profile?.fbp, fbc: profile?.fbc, userId: profile?.user_id, gaClientId: profile?.ga_client_id, value: wh.order_value ? parseFloat(wh.order_value) / 100 : undefined, currency: 'BRL', contentIds: [String(product.product_id || '')], contentName: product.product_name, contentType: 'product', pageUrl: profile?.page_url || `https://${env.SITE_DOMAIN || 'seu-dominio.com.br'}`, orderId: wh.order_id, eventId: `kiwify_${wh.order_id}`, city: profile?.city, state: profile?.state, country: profile?.country, }; ctx.waitUntil(Promise.allSettled([ sendMetaCapi(env, 'Purchase', payload, request, ctx), sendGA4Mp(env, 'purchase', payload, ctx), sendTikTokApi(env, 'CompletePayment', payload, request, ctx), sendPinterestCapi(env, 'Purchase', payload, request, ctx), sendRedditCapi(env, 'Purchase', payload, request, ctx), sendLinkedInCapi(env, 'Purchase', payload, request, ctx), sendSpotifyCapi(env, 'Purchase', payload, request, ctx), saveLead(env, 'Purchase', payload, request, 'kiwify'), sendWhatsApp(env, 'Purchase', payload), ])); return new Response(JSON.stringify({ ok: true }), { status: 200, headers }); } // ── POST /webhook/ticto ─────────────────────────────────────────────────── if (request.method === 'POST' && url.pathname === '/webhook/ticto') { let rawBody; try { rawBody = await request.text(); } catch { return new Response('Leitura de body falhou', { status: 400 }); } if (env.WEBHOOK_SECRET_TICTO) { const sig = request.headers.get('X-Ticto-Signature') || ''; const valid = await verifyHmac(env.WEBHOOK_SECRET_TICTO, rawBody, sig); if (!valid) { return new Response('Unauthorized', { status: 401 }); } } let wh: TictoWebhook; try { wh = JSON.parse(rawBody) as TictoWebhook; } catch { return new Response('JSON inválido', { status: 400 }); } const STATUS_PAID = ['paid', 'approved', 'complete', 'completed']; if (!STATUS_PAID.includes((wh.status || '').toLowerCase())) { return new Response(JSON.stringify({ skipped: `status ${wh.status}` }), { status: 200, headers }); } const customer = wh.customer || {}; const order = wh.order || {}; const item = wh.item || {}; const tracking = wh.tracking || wh.url_params || {}; const valueRaw = order.paid_amount ?? order.total ?? order.amount; const value = valueRaw ? parseFloat(String(valueRaw)) / 100 : undefined; const transactionId = order.hash || order.transaction_hash || order.id; const tcTxId = String(order.hash || order.transaction_hash || order.id || ''); const dupCheck = await processWebhookDuplicateCheck(env, 'ticto', tcTxId, rawBody, { email: customer.email, }); if (dupCheck.duplicate) { return new Response(JSON.stringify({ ok: true, skipped: 'duplicate' }), { status: 200, headers }); } const urlUserId = tracking.user_id || wh.url_params?.user_id; let profile = await getProfileByEmail(env, customer.email || ''); if (!profile && urlUserId && env.DB) { try { profile = await env.DB.prepare( 'SELECT * FROM user_profiles WHERE user_id = ? ORDER BY updated_at DESC LIMIT 1' ).bind(urlUserId).first(); } catch (err: any) { console.error('[POST /webhook/ticto] Error fetching user profile by userId:', { userId: urlUserId, email: customer.email, error: err?.message || String(err), stack: err?.stack, }); } } const fbclid = tracking.fbclid || wh.url_params?.fbclid; const fbc = profile?.fbc || (fbclid ? `fb.1.${Date.now()}.${fbclid}` : undefined); const payload = { email: customer.email, phone: customer.phone, firstName: customer.name?.split(' ')[0], lastName: customer.name?.split(' ').slice(1).join(' ') || undefined, fbp: profile?.fbp, fbc, ttp: profile?.ttp, userId: profile?.user_id, gaClientId: profile?.ga_client_id, value, currency: 'BRL', contentIds: [String(item.product_id || '')], contentName: item.product_name, contentType: 'product', pageUrl: profile?.page_url || `https://${env.SITE_DOMAIN || 'seu-dominio.com.br'}`, orderId: transactionId, eventId: `ticto_${transactionId}`, city: profile?.city, state: profile?.state, country: profile?.country || 'br', utmSource: tracking.utm_source || tracking.src || '', utmMedium: tracking.utm_medium || '', utmCampaign: tracking.utm_campaign || '', utmContent: tracking.utm_content || '', }; ctx.waitUntil(Promise.allSettled([ sendMetaCapi(env, 'Purchase', payload, request, ctx), sendGA4Mp(env, 'purchase', payload, ctx), sendTikTokApi(env, 'CompletePayment', payload, request, ctx), sendPinterestCapi(env, 'Purchase', payload, request, ctx), sendRedditCapi(env, 'Purchase', payload, request, ctx), sendLinkedInCapi(env, 'Purchase', payload, request, ctx), sendSpotifyCapi(env, 'Purchase', payload, request, ctx), saveLead(env, 'Purchase', payload, request, 'ticto'), sendWhatsApp(env, 'Purchase', payload), ])); return new Response(JSON.stringify({ ok: true }), { status: 200, headers }); } // ── GET /webhook/whatsapp — verificação do webhook pela Meta ───────────── if (request.method === 'GET' && url.pathname === '/webhook/whatsapp') { const mode = url.searchParams.get('hub.mode'); const token = url.searchParams.get('hub.verify_token'); const challenge = url.searchParams.get('hub.challenge'); if (mode === 'subscribe' && env.WA_WEBHOOK_VERIFY_TOKEN && token === env.WA_WEBHOOK_VERIFY_TOKEN) { return new Response(challenge, { status: 200, headers: { 'Content-Type': 'text/plain' } }); } return new Response('Forbidden', { status: 403 }); } // ── POST /webhook/whatsapp — mensagens recebidas (CTWA) ────────────────── if (request.method === 'POST' && url.pathname === '/webhook/whatsapp') { let rawBody: string; let body: any; try { rawBody = await request.text(); body = JSON.parse(rawBody); } catch { return new Response('JSON inválido', { status: 400 }); } if (env.META_APP_SECRET) { const sig = request.headers.get('x-hub-signature-256') || ''; const valid = await verifyHmac(env.META_APP_SECRET, rawBody, sig); if (!valid) { return new Response('Unauthorized', { status: 401 }); } } const result = await processWhatsAppWebhook(env, body, request, ctx); // Forward para ZapMan SDR — qualificação de leads via IA if (env.ZAPMAN_WEBHOOK_URL) { const sig = request.headers.get('x-hub-signature-256') || ''; ctx.waitUntil( fetch(env.ZAPMAN_WEBHOOK_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(sig && { 'x-hub-signature-256': sig }), }, body: rawBody, }).catch(() => {}) ); } return new Response(JSON.stringify({ ok: true, ...result }), { status: 200, headers }); } if (url.pathname.startsWith('/api/')) { const authError = requireAdminAuth(request, env, headers); if (authError) return authError; } if (url.pathname === '/api/health' && request.method === 'GET') { return new Response(JSON.stringify(await buildHealthReport(env), null, 2), { headers }); } // ── ML — Segmentação Dinâmica ───────────────────────────────────────────── if (url.pathname === '/api/segmentation/cluster' && request.method === 'POST') { return handleSegmentationCluster(env, request, headers); } if (url.pathname === '/api/segmentation/list' && request.method === 'GET') { return handleSegmentationList(env, request, headers); } if (url.pathname === '/api/segmentation/outliers' && request.method === 'GET') { return handleSegmentationOutliers(env, request, headers); } if (url.pathname === '/api/segmentation/update' && request.method === 'PUT') { return handleSegmentationUpdate(env, request, headers); } // ── ML — Bidding Recommendations ────────────────────────────────────────── if (url.pathname === '/api/bidding/recommend' && request.method === 'POST') { return handleBiddingRecommend(env, request, headers); } if (url.pathname === '/api/bidding/history' && request.method === 'GET') { return handleBiddingHistory(env, request, headers); } if (url.pathname === '/api/bidding/status' && request.method === 'GET') { return handleBiddingStatus(env, request, headers); } // ── ML — A/B Testing de Prompts LTV ────────────────────────────────────── if (url.pathname === '/api/ltv/ab-test/create' && request.method === 'POST') { return handleLtvAbTestCreate(env, request, headers); } if (url.pathname === '/api/ltv/ab-test/list' && request.method === 'GET') { return handleLtvAbTestList(env, request, headers); } if (url.pathname === '/api/ltv/ab-test/results' && request.method === 'GET') { return handleLtvAbTestResults(env, request, headers); } if (url.pathname === '/api/ltv/ab-test/winner' && request.method === 'POST') { return handleLtvAbTestWinner(env, request, headers); } // ── Fraud Detection — Fase 4 ────────────────────────────────────────────── if (url.pathname === '/api/fraud/alerts' && request.method === 'GET') { return handleFraudAlerts(env, request, headers); } if (url.pathname === '/api/fraud/blocklist' && request.method === 'GET') { return handleFraudBlocklist(env, request, headers); } if (url.pathname === '/api/fraud/blocklist/add' && request.method === 'POST') { return handleFraudBlocklistAdd(env, request, headers); } if (url.pathname === '/api/fraud/blocklist/remove' && request.method === 'DELETE') { return handleFraudBlocklistRemove(env, request, headers); } if (url.pathname === '/api/fraud/stats' && request.method === 'GET') { return handleFraudStats(env, request, headers); } // 404 return new Response(JSON.stringify({ error: 'rota não encontrada' }), { status: 404, headers }); }, // ── Cron Handler — Intelligence Agent ──────────────────────────────────────── async scheduled(event: any, env: Env, ctx: ExecutionContext) { const cron = event.cron; const isMonthly = cron === '0 3 1 * *'; ctx.waitUntil(runIntelligenceAgent(env, isMonthly ? 'monthly_audit' : 'weekly_check')); }, // ── Queue Consumer — Retry de eventos com falha ─────────────────────────────── async queue(batch: any, env: Env, ctx: ExecutionContext) { for (const message of batch.messages) { const { eventType, payload, platform } = message.body as { eventType: string; payload: TrackPayload; platform: string; attempt?: number }; try { if (platform === 'meta') await sendMetaCapi(env, eventType, payload, null, ctx); if (platform === 'ga4') await sendGA4Mp(env, eventType, payload, ctx); if (platform === 'tiktok') await sendTikTokApi(env, eventType, payload, null, ctx); if (platform === 'pinterest') await sendPinterestCapi(env, eventType, payload, null, ctx); if (platform === 'reddit') await sendRedditCapi(env, eventType, payload, null, ctx); if (platform === 'linkedin') await sendLinkedInCapi(env, eventType, payload, null, ctx); if (platform === 'spotify') await sendSpotifyCapi(env, eventType, payload, null, ctx); message.ack(); } catch (err: any) { console.error(`[Queue] Falha ao reprocessar ${platform}/${eventType}:`, err?.message || String(err)); message.retry(); } } }, };