cdp-edge 2.0.3 → 2.0.5
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/contracts/agent-versions.json +364 -0
- package/dist/commands/install.js +1 -1
- package/dist/commands/setup.js +326 -111
- package/extracted-skill/tracking-events-generator/INTEGRACAO-COMPLETA.md +89 -0
- package/extracted-skill/tracking-events-generator/MELHORIAS-IMPLEMENTADAS.md +101 -0
- package/extracted-skill/tracking-events-generator/agents/devops-agent.md +11 -0
- package/extracted-skill/tracking-events-generator/agents/intelligence-agent.md +27 -0
- package/extracted-skill/tracking-events-generator/agents/master-orchestrator.md +1 -1
- package/extracted-skill/tracking-events-generator/agents/server-tracking.md +5 -5
- package/extracted-skill/tracking-events-generator/knowledge-base.md +172 -0
- package/package.json +7 -2
- package/server-edge-tracker/INSTALAR.md +27 -3
- package/server-edge-tracker/SEGMENTATION-DOCS.md +69 -0
- package/server-edge-tracker/index.js +791 -0
- package/server-edge-tracker/migrate-v7.sql +64 -0
- package/server-edge-tracker/modules/db.js +531 -0
- package/server-edge-tracker/modules/dispatch/ga4.js +65 -0
- package/server-edge-tracker/modules/dispatch/meta.js +119 -0
- package/server-edge-tracker/modules/dispatch/platforms.js +237 -0
- package/server-edge-tracker/modules/dispatch/tiktok.js +100 -0
- package/server-edge-tracker/modules/dispatch/whatsapp.js +233 -0
- package/server-edge-tracker/modules/intelligence.js +321 -0
- package/server-edge-tracker/modules/ml/bidding.js +245 -0
- package/server-edge-tracker/modules/ml/fraud.js +301 -0
- package/server-edge-tracker/modules/ml/logistic.js +195 -0
- package/server-edge-tracker/modules/ml/ltv.js +420 -0
- package/server-edge-tracker/modules/ml/matchquality.js +176 -0
- package/server-edge-tracker/modules/ml/segmentation.js +316 -0
- package/server-edge-tracker/modules/utils.js +89 -0
- package/server-edge-tracker/schema-indexes.sql +67 -0
- package/server-edge-tracker/worker.js +395 -4
- package/server-edge-tracker/wrangler.toml +15 -0
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDP Edge — index.js (ES Module Entry Point)
|
|
3
|
+
*
|
|
4
|
+
* Este arquivo é o novo entry point modular do Worker.
|
|
5
|
+
* Para usá-lo, altere em wrangler.toml:
|
|
6
|
+
* main = "worker.js" → main = "index.js"
|
|
7
|
+
*
|
|
8
|
+
* O worker.js original permanece intacto como fallback.
|
|
9
|
+
* Todos os módulos ficam em ./modules/
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ── Utilitários base ──────────────────────────────────────────────────────────
|
|
13
|
+
import {
|
|
14
|
+
isAllowedOrigin,
|
|
15
|
+
corsHeaders,
|
|
16
|
+
sha256,
|
|
17
|
+
META_TO_GA4,
|
|
18
|
+
VALID_EVENT_NAMES,
|
|
19
|
+
} from './modules/utils.js';
|
|
20
|
+
|
|
21
|
+
// ── Banco de dados (D1) ───────────────────────────────────────────────────────
|
|
22
|
+
import {
|
|
23
|
+
saveLead,
|
|
24
|
+
upsertProfile,
|
|
25
|
+
resolveDeviceGraph,
|
|
26
|
+
fireAutomation,
|
|
27
|
+
getProfileByEmail,
|
|
28
|
+
enrichGeoFromEdge,
|
|
29
|
+
writeAuditLog,
|
|
30
|
+
generateEdgeFingerprint,
|
|
31
|
+
saveEdgeFingerprint,
|
|
32
|
+
resurrectUTM,
|
|
33
|
+
upsertLtvProfile,
|
|
34
|
+
} from './modules/db.js';
|
|
35
|
+
|
|
36
|
+
// ── Dispatch — plataformas de ads ─────────────────────────────────────────────
|
|
37
|
+
import { sendMetaCapi } from './modules/dispatch/meta.js';
|
|
38
|
+
import { sendGA4Mp } from './modules/dispatch/ga4.js';
|
|
39
|
+
import { sendTikTokApi } from './modules/dispatch/tiktok.js';
|
|
40
|
+
import {
|
|
41
|
+
sendPinterestCapi,
|
|
42
|
+
sendRedditCapi,
|
|
43
|
+
sendLinkedInCapi,
|
|
44
|
+
sendSpotifyCapi,
|
|
45
|
+
} from './modules/dispatch/platforms.js';
|
|
46
|
+
import {
|
|
47
|
+
sendWhatsApp,
|
|
48
|
+
processWhatsAppWebhook,
|
|
49
|
+
verifyHmac,
|
|
50
|
+
} from './modules/dispatch/whatsapp.js';
|
|
51
|
+
|
|
52
|
+
// ── ML — LTV + A/B Testing ────────────────────────────────────────────────────
|
|
53
|
+
import {
|
|
54
|
+
predictLtv,
|
|
55
|
+
getLtvAbVariation,
|
|
56
|
+
recordAbAssignment,
|
|
57
|
+
handleLtvAbTestCreate,
|
|
58
|
+
handleLtvAbTestList,
|
|
59
|
+
handleLtvAbTestResults,
|
|
60
|
+
handleLtvAbTestWinner,
|
|
61
|
+
} from './modules/ml/ltv.js';
|
|
62
|
+
|
|
63
|
+
// ── ML — Segmentação ──────────────────────────────────────────────────────────
|
|
64
|
+
import {
|
|
65
|
+
handleSegmentationCluster,
|
|
66
|
+
handleSegmentationList,
|
|
67
|
+
handleSegmentationOutliers,
|
|
68
|
+
handleSegmentationUpdate,
|
|
69
|
+
} from './modules/ml/segmentation.js';
|
|
70
|
+
|
|
71
|
+
// ── ML — Bidding ──────────────────────────────────────────────────────────────
|
|
72
|
+
import {
|
|
73
|
+
handleBiddingRecommend,
|
|
74
|
+
handleBiddingHistory,
|
|
75
|
+
handleBiddingStatus,
|
|
76
|
+
} from './modules/ml/bidding.js';
|
|
77
|
+
|
|
78
|
+
// ── ML — Fraud Detection ──────────────────────────────────────────────────────
|
|
79
|
+
import {
|
|
80
|
+
checkFraudGate,
|
|
81
|
+
logFraudSignal,
|
|
82
|
+
handleFraudAlerts,
|
|
83
|
+
handleFraudBlocklist,
|
|
84
|
+
handleFraudBlocklistAdd,
|
|
85
|
+
handleFraudBlocklistRemove,
|
|
86
|
+
handleFraudStats,
|
|
87
|
+
} from './modules/ml/fraud.js';
|
|
88
|
+
|
|
89
|
+
// ── Intelligence Agent (Cron) ─────────────────────────────────────────────────
|
|
90
|
+
import {
|
|
91
|
+
runIntelligenceAgent,
|
|
92
|
+
buildGoogleCustomerMatchExport,
|
|
93
|
+
} from './modules/intelligence.js';
|
|
94
|
+
|
|
95
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
96
|
+
// HANDLER PRINCIPAL
|
|
97
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
98
|
+
export default {
|
|
99
|
+
|
|
100
|
+
async fetch(request, env, ctx) {
|
|
101
|
+
const origin = request.headers.get('Origin') || '';
|
|
102
|
+
const headers = {
|
|
103
|
+
'Content-Type': 'application/json',
|
|
104
|
+
...corsHeaders(origin, env.SITE_DOMAIN),
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Preflight CORS
|
|
108
|
+
if (request.method === 'OPTIONS') {
|
|
109
|
+
return new Response(null, { status: 204, headers });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const url = new URL(request.url);
|
|
113
|
+
|
|
114
|
+
// ── Rate Limiter — camada 0, antes do Fraud Gate ─────────────────────────
|
|
115
|
+
if (url.pathname === '/track' && request.method === 'POST' && env.RATE_LIMITER) {
|
|
116
|
+
const ip = request.headers.get('CF-Connecting-IP')
|
|
117
|
+
|| request.headers.get('X-Forwarded-For')?.split(',')[0].trim()
|
|
118
|
+
|| '0.0.0.0';
|
|
119
|
+
const { success } = await env.RATE_LIMITER.limit({ key: ip });
|
|
120
|
+
if (!success) {
|
|
121
|
+
return new Response(JSON.stringify({ status: 'ok', queued: true }), { status: 200, headers });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Fraud Gate — Fase 4 (apenas em /track) ────────────────────────────────
|
|
126
|
+
// Roda ANTES de qualquer processamento de evento
|
|
127
|
+
// Silent drop (200) — bots não sabem que foram detectados
|
|
128
|
+
if (url.pathname === '/track' && request.method === 'POST') {
|
|
129
|
+
let trackBodyForFraud;
|
|
130
|
+
try {
|
|
131
|
+
const cloned = request.clone();
|
|
132
|
+
trackBodyForFraud = await cloned.json().catch(() => ({}));
|
|
133
|
+
} catch { trackBodyForFraud = {}; }
|
|
134
|
+
|
|
135
|
+
const fraudResult = await checkFraudGate(env, request, trackBodyForFraud);
|
|
136
|
+
if (!fraudResult.allowed) {
|
|
137
|
+
ctx.waitUntil(logFraudSignal(env, request, trackBodyForFraud, fraudResult));
|
|
138
|
+
return new Response(JSON.stringify({ status: 'ok', queued: true }), { status: 200, headers });
|
|
139
|
+
}
|
|
140
|
+
if (fraudResult.action === 'flagged') {
|
|
141
|
+
ctx.waitUntil(logFraudSignal(env, request, trackBodyForFraud, fraudResult));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── GET /export/customer-match ────────────────────────────────────────────
|
|
146
|
+
if (request.method === 'GET' && url.pathname === '/export/customer-match') {
|
|
147
|
+
const authHeader = request.headers.get('Authorization') || '';
|
|
148
|
+
const token = authHeader.replace('Bearer ', '');
|
|
149
|
+
if (!env.META_ACCESS_TOKEN || token !== env.META_ACCESS_TOKEN) {
|
|
150
|
+
return new Response('Unauthorized', { status: 401 });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const rows = await buildGoogleCustomerMatchExport(env);
|
|
154
|
+
return new Response(JSON.stringify({ total: rows.length, data: rows }, null, 2), {
|
|
155
|
+
headers: { ...headers, 'Content-Disposition': 'attachment; filename="customer-match.json"' },
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── GET /health ───────────────────────────────────────────────────────────
|
|
160
|
+
if (request.method === 'GET' && url.pathname === '/health') {
|
|
161
|
+
const results = {};
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
await env.DB.prepare('SELECT 1').run();
|
|
165
|
+
results.d1 = 'ok';
|
|
166
|
+
} catch (err) {
|
|
167
|
+
results.d1 = `FAILED: ${err.message}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
await env.GEO_CACHE.get('__health_check__');
|
|
172
|
+
results.kv = 'ok';
|
|
173
|
+
} catch (err) {
|
|
174
|
+
results.kv = `FAILED: ${err.message}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
await env.AI.run('@cf/meta/llama-3.1-8b-instruct', {
|
|
179
|
+
messages: [{ role: 'user', content: 'ping' }],
|
|
180
|
+
max_tokens: 1,
|
|
181
|
+
});
|
|
182
|
+
results.ai = 'ok';
|
|
183
|
+
} catch (err) {
|
|
184
|
+
results.ai = `FAILED: ${err.message}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const vars = {
|
|
188
|
+
META_PIXEL_ID: env.META_PIXEL_ID ? 'set' : 'MISSING',
|
|
189
|
+
GA4_MEASUREMENT_ID: env.GA4_MEASUREMENT_ID ? 'set' : 'MISSING',
|
|
190
|
+
TIKTOK_PIXEL_ID: env.TIKTOK_PIXEL_ID ? 'set' : 'MISSING',
|
|
191
|
+
SITE_DOMAIN: env.SITE_DOMAIN ? 'set' : 'MISSING',
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const secrets = {
|
|
195
|
+
META_ACCESS_TOKEN: env.META_ACCESS_TOKEN ? 'set' : 'MISSING',
|
|
196
|
+
GA4_API_SECRET: env.GA4_API_SECRET ? 'set' : 'MISSING',
|
|
197
|
+
WA_WEBHOOK_VERIFY_TOKEN: env.WA_WEBHOOK_VERIFY_TOKEN ? 'set' : 'MISSING',
|
|
198
|
+
WHATSAPP_ACCESS_TOKEN: env.WHATSAPP_ACCESS_TOKEN ? 'set' : 'not set (optional - only for auto-reply)',
|
|
199
|
+
WHATSAPP_PHONE_NUMBER_ID: env.WHATSAPP_PHONE_NUMBER_ID ? 'set' : 'not set (optional - only for auto-reply)',
|
|
200
|
+
WA_NOTIFY_NUMBER: env.WA_NOTIFY_NUMBER ? 'set' : 'not set (optional - only for auto-reply)',
|
|
201
|
+
TIKTOK_ACCESS_TOKEN: env.TIKTOK_ACCESS_TOKEN ? 'set' : 'not set (optional)',
|
|
202
|
+
CALLMEBOT_PHONE: env.CALLMEBOT_PHONE ? 'set' : 'not set (optional)',
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const hasMissing =
|
|
206
|
+
Object.values(vars).includes('MISSING') ||
|
|
207
|
+
Object.values(secrets).includes('MISSING') ||
|
|
208
|
+
results.d1 !== 'ok';
|
|
209
|
+
|
|
210
|
+
return new Response(JSON.stringify({
|
|
211
|
+
status: hasMissing ? 'degraded' : 'ok',
|
|
212
|
+
timestamp: new Date().toISOString(),
|
|
213
|
+
bindings: results,
|
|
214
|
+
vars,
|
|
215
|
+
secrets,
|
|
216
|
+
}, null, 2), { headers });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── POST /track ───────────────────────────────────────────────────────────
|
|
220
|
+
if (request.method === 'POST' && url.pathname === '/track') {
|
|
221
|
+
// Reject oversized payloads before reading body (64 KB limit)
|
|
222
|
+
const contentLength = parseInt(request.headers.get('Content-Length') || '0', 10);
|
|
223
|
+
if (contentLength > 65536) {
|
|
224
|
+
return new Response(JSON.stringify({ error: 'Payload muito grande' }), { status: 413, headers });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let body;
|
|
228
|
+
try {
|
|
229
|
+
body = await request.json();
|
|
230
|
+
} catch {
|
|
231
|
+
return new Response(JSON.stringify({ error: 'JSON inválido' }), { status: 400, headers });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (typeof body !== 'object' || Array.isArray(body) || body === null) {
|
|
235
|
+
return new Response(JSON.stringify({ error: 'Payload inválido' }), { status: 400, headers });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const STR_FIELDS = ['email','phone','firstName','lastName','city','state','zip','userId',
|
|
239
|
+
'utmSource','utmMedium','utmCampaign','utmContent','utmTerm',
|
|
240
|
+
'fbclid','ttclid','gclid','transactionId','productName','currency'];
|
|
241
|
+
|
|
242
|
+
const { eventName, behavioral_data, ...payload } = body;
|
|
243
|
+
|
|
244
|
+
if (!eventName) {
|
|
245
|
+
return new Response(JSON.stringify({ error: 'eventName é obrigatório' }), { status: 400, headers });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (typeof eventName !== 'string' || eventName.length > 64 || !VALID_EVENT_NAMES.has(eventName)) {
|
|
249
|
+
return new Response(JSON.stringify({ error: `eventName desconhecido: ${eventName.slice(0, 64)}` }), { status: 400, headers });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
for (const field of STR_FIELDS) {
|
|
253
|
+
if (payload[field] !== undefined && payload[field] !== null) {
|
|
254
|
+
if (typeof payload[field] !== 'string' || payload[field].length > 512) {
|
|
255
|
+
return new Response(JSON.stringify({ error: `Campo inválido: ${field}` }), { status: 400, headers });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (payload.value !== undefined && payload.value !== null) {
|
|
261
|
+
const v = Number(payload.value);
|
|
262
|
+
if (isNaN(v) || v < 0 || v > 9_999_999) {
|
|
263
|
+
return new Response(JSON.stringify({ error: 'value fora do intervalo permitido' }), { status: 400, headers });
|
|
264
|
+
}
|
|
265
|
+
payload.value = v;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ── Extrair dados comportamentais do browser ──────────────────────────
|
|
269
|
+
if (behavioral_data) {
|
|
270
|
+
payload.engagementScore = behavioral_data.engagement_score ?? behavioral_data.totalScore ?? null;
|
|
271
|
+
payload.intentionLevel = behavioral_data.intention_level ?? null;
|
|
272
|
+
payload.userScore = behavioral_data.user_score ?? null;
|
|
273
|
+
payload.email = payload.email || behavioral_data.email || null;
|
|
274
|
+
payload.phone = payload.phone || behavioral_data.phone || null;
|
|
275
|
+
payload.firstName = payload.firstName || behavioral_data.first_name || behavioral_data.firstName || null;
|
|
276
|
+
payload.lastName = payload.lastName || behavioral_data.last_name || behavioral_data.lastName || null;
|
|
277
|
+
payload.city = payload.city || behavioral_data.city || null;
|
|
278
|
+
payload.state = payload.state || behavioral_data.state || null;
|
|
279
|
+
payload.zip = payload.zip || behavioral_data.zip || null;
|
|
280
|
+
payload.dob = payload.dob || behavioral_data.dob || null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ── Edge Fingerprint + UTM Resurrection ──────────────────────────────
|
|
284
|
+
const fingerprint = await generateEdgeFingerprint(request);
|
|
285
|
+
payload.utmRestored = false;
|
|
286
|
+
|
|
287
|
+
if (fingerprint) {
|
|
288
|
+
if (payload.utmSource) {
|
|
289
|
+
ctx.waitUntil(saveEdgeFingerprint(env.DB, fingerprint, payload.userId, payload));
|
|
290
|
+
} else {
|
|
291
|
+
const recovered = await resurrectUTM(env.DB, fingerprint);
|
|
292
|
+
if (recovered) {
|
|
293
|
+
payload.utmSource = payload.utmSource || recovered.utm_source;
|
|
294
|
+
payload.utmMedium = payload.utmMedium || recovered.utm_medium;
|
|
295
|
+
payload.utmCampaign = payload.utmCampaign || recovered.utm_campaign;
|
|
296
|
+
payload.utmContent = payload.utmContent || recovered.utm_content;
|
|
297
|
+
payload.utmTerm = payload.utmTerm || recovered.utm_term;
|
|
298
|
+
payload.utmRestored = true;
|
|
299
|
+
console.log(`[UTM Resurrection] Recovered: ${recovered.utm_source}/${recovered.utm_medium}/${recovered.utm_campaign}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ── Bot Mitigation ────────────────────────────────────────────────────
|
|
305
|
+
const botScoreStr = request.cf?.botManagement?.score;
|
|
306
|
+
const cfBotScore = botScoreStr !== undefined ? parseInt(botScoreStr) : 100;
|
|
307
|
+
const ua = (request.headers.get('User-Agent') || '').toLowerCase();
|
|
308
|
+
const isBotPattern = /bot|crawler|spider|lighthouse|postman|curl|inspect|headless/i.test(ua);
|
|
309
|
+
|
|
310
|
+
const isBot = cfBotScore < 30 || isBotPattern;
|
|
311
|
+
payload.botScore = isBot ? 2 : (cfBotScore < 60 ? 1 : 0);
|
|
312
|
+
|
|
313
|
+
if (isBot && !['Contact', 'Lead', 'Purchase'].includes(eventName)) {
|
|
314
|
+
return new Response(JSON.stringify({ ok: true, skipped_due_to_bot: true }), { status: 200, headers });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ── Edge Geo Enrichment ───────────────────────────────────────────────
|
|
318
|
+
const geoData = await enrichGeoFromEdge(request, env, payload);
|
|
319
|
+
|
|
320
|
+
// ── First-Party Cookie (Identity Resolution) ──────────────────────────
|
|
321
|
+
const cookieHeader = request.headers.get('Cookie') || '';
|
|
322
|
+
const cdpUidMatch = cookieHeader.match(/_cdp_uid=([^;]+)/);
|
|
323
|
+
const finalUserId = payload.userId || (cdpUidMatch ? cdpUidMatch[1] : crypto.randomUUID());
|
|
324
|
+
payload.userId = finalUserId;
|
|
325
|
+
|
|
326
|
+
const ga4Name = META_TO_GA4[eventName] || eventName.toLowerCase();
|
|
327
|
+
|
|
328
|
+
// ── LTV Prediction (+ A/B Testing de Prompts) ─────────────────────────
|
|
329
|
+
const LTV_EVENTS = ['Lead', 'Contact', 'Schedule', 'CompleteRegistration'];
|
|
330
|
+
if (LTV_EVENTS.includes(eventName) && !payload.value) {
|
|
331
|
+
const abVariation = await getLtvAbVariation(env);
|
|
332
|
+
const ltv = await predictLtv(env, payload, request, abVariation?.system_prompt || null);
|
|
333
|
+
payload.value = ltv.value;
|
|
334
|
+
payload.currency = payload.currency || 'BRL';
|
|
335
|
+
payload.ltvClass = ltv.class;
|
|
336
|
+
payload.ltvScore = ltv.score;
|
|
337
|
+
ctx.waitUntil(upsertLtvProfile(env, payload.userId, ltv));
|
|
338
|
+
if (abVariation) {
|
|
339
|
+
const emailHash = payload.email
|
|
340
|
+
? await sha256(payload.email.trim().toLowerCase())
|
|
341
|
+
: null;
|
|
342
|
+
ctx.waitUntil(
|
|
343
|
+
recordAbAssignment(
|
|
344
|
+
env,
|
|
345
|
+
payload.userId,
|
|
346
|
+
abVariation.variation_id,
|
|
347
|
+
abVariation.test_id,
|
|
348
|
+
ltv.value,
|
|
349
|
+
ltv.class,
|
|
350
|
+
emailHash,
|
|
351
|
+
)
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Cross-Device Graph — background
|
|
357
|
+
if (env.DB && payload.userId && (payload.email || payload.phone)) {
|
|
358
|
+
ctx.waitUntil(resolveDeviceGraph(env.DB, payload.userId, payload.email, payload.phone));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// R2 Audit Log — background
|
|
362
|
+
ctx.waitUntil(writeAuditLog(env, eventName, payload, geoData));
|
|
363
|
+
|
|
364
|
+
// Disparar tudo em paralelo
|
|
365
|
+
const WHATSAPP_NOTIFY_EVENTS = ['Lead', 'Purchase', 'CompleteRegistration'];
|
|
366
|
+
const [metaRes, ga4Res, ttRes] = await Promise.allSettled([
|
|
367
|
+
sendMetaCapi(env, eventName, payload, request, ctx),
|
|
368
|
+
sendGA4Mp(env, ga4Name, payload, ctx),
|
|
369
|
+
sendTikTokApi(env, eventName, payload, request, ctx),
|
|
370
|
+
saveLead(env, eventName, payload, request, 'website'),
|
|
371
|
+
upsertProfile(env, eventName, payload, request),
|
|
372
|
+
...(WHATSAPP_NOTIFY_EVENTS.includes(eventName)
|
|
373
|
+
? [sendWhatsApp(env, eventName, payload)]
|
|
374
|
+
: []),
|
|
375
|
+
]);
|
|
376
|
+
|
|
377
|
+
// Automação de mensagens
|
|
378
|
+
const AUTOMATION_EVENTS = ['Lead', 'Purchase', 'InitiateCheckout'];
|
|
379
|
+
if (AUTOMATION_EVENTS.includes(eventName) && env.DB) {
|
|
380
|
+
ctx.waitUntil(
|
|
381
|
+
(async () => {
|
|
382
|
+
try {
|
|
383
|
+
const lastLead = await env.DB
|
|
384
|
+
.prepare(`SELECT id FROM leads WHERE event_id = ?1 LIMIT 1`)
|
|
385
|
+
.bind(payload.eventId || payload.event_id || '')
|
|
386
|
+
.first();
|
|
387
|
+
const leadId = lastLead?.id ?? null;
|
|
388
|
+
if (leadId) await fireAutomation(env, eventName, leadId, payload);
|
|
389
|
+
} catch (e) { console.error('[Automation] lead lookup error:', e.message); }
|
|
390
|
+
})()
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Edge Personalization
|
|
395
|
+
let currentScore = 0;
|
|
396
|
+
if (env.DB && payload.userId) {
|
|
397
|
+
try {
|
|
398
|
+
const profileRow = await env.DB.prepare('SELECT score FROM user_profiles WHERE user_id = ?').bind(payload.userId).first();
|
|
399
|
+
if (profileRow) currentScore = profileRow.score;
|
|
400
|
+
} catch {}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const resHeaders = new Headers(headers);
|
|
404
|
+
resHeaders.set('Set-Cookie', `_cdp_uid=${finalUserId}; HttpOnly; Secure; SameSite=Lax; Max-Age=31536000; Path=/; Domain=.${env.SITE_DOMAIN}`);
|
|
405
|
+
|
|
406
|
+
return new Response(JSON.stringify({
|
|
407
|
+
ok: true,
|
|
408
|
+
userProfile: { score: currentScore, user_id: finalUserId },
|
|
409
|
+
meta: metaRes.value ?? { error: metaRes.reason?.message },
|
|
410
|
+
ga4: ga4Res.value ?? { error: ga4Res.reason?.message },
|
|
411
|
+
tiktok: ttRes.value ?? { error: ttRes.reason?.message },
|
|
412
|
+
}), { status: 200, headers: resHeaders });
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ── POST /webhook/hotmart ─────────────────────────────────────────────────
|
|
416
|
+
if (request.method === 'POST' && url.pathname === '/webhook/hotmart') {
|
|
417
|
+
if (env.WEBHOOK_SECRET_HOTMART) {
|
|
418
|
+
const token = request.headers.get('X-Hotmart-Webhook-Token') || '';
|
|
419
|
+
if (token !== env.WEBHOOK_SECRET_HOTMART) {
|
|
420
|
+
return new Response('Unauthorized', { status: 401 });
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
let wh;
|
|
425
|
+
try { wh = await request.json(); } catch {
|
|
426
|
+
return new Response('JSON inválido', { status: 400 });
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const data = wh.data || wh;
|
|
430
|
+
const buyer = data.buyer || {};
|
|
431
|
+
const purchase = data.purchase || {};
|
|
432
|
+
const product = data.product || {};
|
|
433
|
+
|
|
434
|
+
if (!['APPROVED', 'COMPLETE', 'approved', 'complete'].includes(purchase.status)) {
|
|
435
|
+
return new Response(JSON.stringify({ skipped: `status ${purchase.status}` }), { status: 200, headers });
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const hmTxId = String(purchase.transaction || '');
|
|
439
|
+
if (hmTxId && env.DB) {
|
|
440
|
+
try {
|
|
441
|
+
const dup = await env.DB.prepare(
|
|
442
|
+
'SELECT id FROM webhook_events WHERE transaction_id = ? AND status = ?'
|
|
443
|
+
).bind(hmTxId, 'processed').first();
|
|
444
|
+
if (dup) {
|
|
445
|
+
return new Response(JSON.stringify({ ok: true, skipped: 'duplicate' }), { status: 200, headers });
|
|
446
|
+
}
|
|
447
|
+
} catch {}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const profile = await getProfileByEmail(env, buyer.email);
|
|
451
|
+
|
|
452
|
+
const payload = {
|
|
453
|
+
email: buyer.email,
|
|
454
|
+
phone: buyer.phone,
|
|
455
|
+
firstName: buyer.name?.split(' ')[0],
|
|
456
|
+
lastName: buyer.name?.split(' ').slice(1).join(' ') || undefined,
|
|
457
|
+
fbp: profile?.fbp,
|
|
458
|
+
fbc: profile?.fbc,
|
|
459
|
+
userId: profile?.user_id,
|
|
460
|
+
gaClientId: profile?.ga_client_id,
|
|
461
|
+
value: purchase.price?.value,
|
|
462
|
+
currency: purchase.price?.currency_value || 'BRL',
|
|
463
|
+
contentIds: [String(product.id || product.ucode || '')],
|
|
464
|
+
contentName: product.name,
|
|
465
|
+
contentType: 'product',
|
|
466
|
+
pageUrl: profile?.page_url || `https://${env.SITE_DOMAIN || 'seu-dominio.com.br'}`,
|
|
467
|
+
orderId: purchase.transaction,
|
|
468
|
+
eventId: `hotmart_${purchase.transaction}`,
|
|
469
|
+
city: profile?.city,
|
|
470
|
+
state: profile?.state,
|
|
471
|
+
country: profile?.country,
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
if (hmTxId && env.DB) {
|
|
475
|
+
try {
|
|
476
|
+
await env.DB.prepare(
|
|
477
|
+
'INSERT OR IGNORE INTO webhook_events (platform, transaction_id, email, status, raw_payload) VALUES (?,?,?,?,?)'
|
|
478
|
+
).bind('hotmart', hmTxId, buyer.email || null, 'processed', JSON.stringify(wh)).run();
|
|
479
|
+
} catch {}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
ctx.waitUntil(Promise.allSettled([
|
|
483
|
+
sendMetaCapi(env, 'Purchase', payload, request, ctx),
|
|
484
|
+
sendGA4Mp(env, 'purchase', payload, ctx),
|
|
485
|
+
sendTikTokApi(env, 'CompletePayment', payload, request, ctx),
|
|
486
|
+
saveLead(env, 'Purchase', payload, request, 'hotmart'),
|
|
487
|
+
sendWhatsApp(env, 'Purchase', payload),
|
|
488
|
+
]));
|
|
489
|
+
|
|
490
|
+
return new Response(JSON.stringify({ ok: true }), { status: 200, headers });
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ── POST /webhook/kiwify ──────────────────────────────────────────────────
|
|
494
|
+
if (request.method === 'POST' && url.pathname === '/webhook/kiwify') {
|
|
495
|
+
if (env.WEBHOOK_SECRET_KIWIFY) {
|
|
496
|
+
const token = request.headers.get('X-Kiwify-Event-Token') || '';
|
|
497
|
+
if (token !== env.WEBHOOK_SECRET_KIWIFY) {
|
|
498
|
+
return new Response('Unauthorized', { status: 401 });
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
let wh;
|
|
503
|
+
try { wh = await request.json(); } catch {
|
|
504
|
+
return new Response('JSON inválido', { status: 400 });
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (wh.order_status !== 'paid' && wh.order_status !== 'approved') {
|
|
508
|
+
return new Response(JSON.stringify({ skipped: `status ${wh.order_status}` }), { status: 200, headers });
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const kwTxId = String(wh.order_id || '');
|
|
512
|
+
if (kwTxId && env.DB) {
|
|
513
|
+
try {
|
|
514
|
+
const dup = await env.DB.prepare(
|
|
515
|
+
'SELECT id FROM webhook_events WHERE transaction_id = ? AND status = ?'
|
|
516
|
+
).bind(kwTxId, 'processed').first();
|
|
517
|
+
if (dup) {
|
|
518
|
+
return new Response(JSON.stringify({ ok: true, skipped: 'duplicate' }), { status: 200, headers });
|
|
519
|
+
}
|
|
520
|
+
} catch {}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const customer = wh.Customer || {};
|
|
524
|
+
const product = wh.Product || {};
|
|
525
|
+
const profile = await getProfileByEmail(env, customer.email);
|
|
526
|
+
|
|
527
|
+
const payload = {
|
|
528
|
+
email: customer.email,
|
|
529
|
+
phone: customer.mobile,
|
|
530
|
+
firstName: customer.full_name?.split(' ')[0],
|
|
531
|
+
lastName: customer.full_name?.split(' ').slice(1).join(' ') || undefined,
|
|
532
|
+
fbp: profile?.fbp,
|
|
533
|
+
fbc: profile?.fbc,
|
|
534
|
+
userId: profile?.user_id,
|
|
535
|
+
gaClientId: profile?.ga_client_id,
|
|
536
|
+
value: wh.order_value ? parseFloat(wh.order_value) / 100 : undefined,
|
|
537
|
+
currency: 'BRL',
|
|
538
|
+
contentIds: [String(product.product_id || '')],
|
|
539
|
+
contentName: product.product_name,
|
|
540
|
+
contentType: 'product',
|
|
541
|
+
pageUrl: profile?.page_url || `https://${env.SITE_DOMAIN || 'seu-dominio.com.br'}`,
|
|
542
|
+
orderId: wh.order_id,
|
|
543
|
+
eventId: `kiwify_${wh.order_id}`,
|
|
544
|
+
city: profile?.city,
|
|
545
|
+
state: profile?.state,
|
|
546
|
+
country: profile?.country,
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
if (kwTxId && env.DB) {
|
|
550
|
+
try {
|
|
551
|
+
await env.DB.prepare(
|
|
552
|
+
'INSERT OR IGNORE INTO webhook_events (platform, transaction_id, email, status, raw_payload) VALUES (?,?,?,?,?)'
|
|
553
|
+
).bind('kiwify', kwTxId, customer.email || null, 'processed', JSON.stringify(wh)).run();
|
|
554
|
+
} catch {}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
ctx.waitUntil(Promise.allSettled([
|
|
558
|
+
sendMetaCapi(env, 'Purchase', payload, request, ctx),
|
|
559
|
+
sendGA4Mp(env, 'purchase', payload, ctx),
|
|
560
|
+
sendTikTokApi(env, 'CompletePayment', payload, request, ctx),
|
|
561
|
+
sendPinterestCapi(env, 'Purchase', payload, request, ctx),
|
|
562
|
+
sendRedditCapi(env, 'Purchase', payload, request, ctx),
|
|
563
|
+
sendLinkedInCapi(env, 'Purchase', payload, request, ctx),
|
|
564
|
+
sendSpotifyCapi(env, 'Purchase', payload, request, ctx),
|
|
565
|
+
saveLead(env, 'Purchase', payload, request, 'kiwify'),
|
|
566
|
+
sendWhatsApp(env, 'Purchase', payload),
|
|
567
|
+
]));
|
|
568
|
+
|
|
569
|
+
return new Response(JSON.stringify({ ok: true }), { status: 200, headers });
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// ── POST /webhook/ticto ───────────────────────────────────────────────────
|
|
573
|
+
if (request.method === 'POST' && url.pathname === '/webhook/ticto') {
|
|
574
|
+
let rawBody;
|
|
575
|
+
try { rawBody = await request.text(); } catch {
|
|
576
|
+
return new Response('Leitura de body falhou', { status: 400 });
|
|
577
|
+
}
|
|
578
|
+
if (env.WEBHOOK_SECRET_TICTO) {
|
|
579
|
+
const sig = request.headers.get('X-Ticto-Signature') || '';
|
|
580
|
+
const valid = await verifyHmac(env.WEBHOOK_SECRET_TICTO, rawBody, sig);
|
|
581
|
+
if (!valid) {
|
|
582
|
+
return new Response('Unauthorized', { status: 401 });
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
let wh;
|
|
587
|
+
try { wh = JSON.parse(rawBody); } catch {
|
|
588
|
+
return new Response('JSON inválido', { status: 400 });
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const STATUS_PAID = ['paid', 'approved', 'complete', 'completed'];
|
|
592
|
+
if (!STATUS_PAID.includes((wh.status || '').toLowerCase())) {
|
|
593
|
+
return new Response(JSON.stringify({ skipped: `status ${wh.status}` }), { status: 200, headers });
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const customer = wh.customer || {};
|
|
597
|
+
const order = wh.order || {};
|
|
598
|
+
const item = wh.item || {};
|
|
599
|
+
const tracking = wh.tracking || wh.url_params || {};
|
|
600
|
+
|
|
601
|
+
const valueRaw = order.paid_amount ?? order.total ?? order.amount;
|
|
602
|
+
const value = valueRaw ? parseFloat(valueRaw) / 100 : undefined;
|
|
603
|
+
const transactionId = order.hash || order.transaction_hash || order.id;
|
|
604
|
+
const tcTxId = String(order.hash || order.transaction_hash || order.id || '');
|
|
605
|
+
|
|
606
|
+
if (tcTxId && env.DB) {
|
|
607
|
+
try {
|
|
608
|
+
const dup = await env.DB.prepare(
|
|
609
|
+
'SELECT id FROM webhook_events WHERE transaction_id = ? AND status = ?'
|
|
610
|
+
).bind(tcTxId, 'processed').first();
|
|
611
|
+
if (dup) {
|
|
612
|
+
return new Response(JSON.stringify({ ok: true, skipped: 'duplicate' }), { status: 200, headers });
|
|
613
|
+
}
|
|
614
|
+
} catch {}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const urlUserId = tracking.user_id || wh.url_params?.user_id;
|
|
618
|
+
let profile = await getProfileByEmail(env, customer.email);
|
|
619
|
+
if (!profile && urlUserId && env.DB) {
|
|
620
|
+
try {
|
|
621
|
+
profile = await env.DB.prepare(
|
|
622
|
+
'SELECT * FROM user_profiles WHERE user_id = ? ORDER BY updated_at DESC LIMIT 1'
|
|
623
|
+
).bind(urlUserId).first();
|
|
624
|
+
} catch {}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const fbclid = tracking.fbclid || wh.url_params?.fbclid;
|
|
628
|
+
const fbc = profile?.fbc || (fbclid ? `fb.1.${Date.now()}.${fbclid}` : undefined);
|
|
629
|
+
|
|
630
|
+
const payload = {
|
|
631
|
+
email: customer.email,
|
|
632
|
+
phone: customer.phone,
|
|
633
|
+
firstName: customer.name?.split(' ')[0],
|
|
634
|
+
lastName: customer.name?.split(' ').slice(1).join(' ') || undefined,
|
|
635
|
+
fbp: profile?.fbp,
|
|
636
|
+
fbc,
|
|
637
|
+
ttp: profile?.ttp,
|
|
638
|
+
userId: profile?.user_id,
|
|
639
|
+
gaClientId: profile?.ga_client_id,
|
|
640
|
+
value,
|
|
641
|
+
currency: 'BRL',
|
|
642
|
+
contentIds: [String(item.product_id || '')],
|
|
643
|
+
contentName: item.product_name,
|
|
644
|
+
contentType: 'product',
|
|
645
|
+
pageUrl: profile?.page_url || `https://${env.SITE_DOMAIN || 'seu-dominio.com.br'}`,
|
|
646
|
+
orderId: transactionId,
|
|
647
|
+
eventId: `ticto_${transactionId}`,
|
|
648
|
+
city: profile?.city,
|
|
649
|
+
state: profile?.state,
|
|
650
|
+
country: profile?.country || 'br',
|
|
651
|
+
utmSource: tracking.utm_source || tracking.src || '',
|
|
652
|
+
utmMedium: tracking.utm_medium || '',
|
|
653
|
+
utmCampaign: tracking.utm_campaign || '',
|
|
654
|
+
utmContent: tracking.utm_content || '',
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
if (tcTxId && env.DB) {
|
|
658
|
+
try {
|
|
659
|
+
await env.DB.prepare(
|
|
660
|
+
'INSERT OR IGNORE INTO webhook_events (platform, transaction_id, email, status, raw_payload) VALUES (?,?,?,?,?)'
|
|
661
|
+
).bind('ticto', tcTxId, customer.email || null, 'processed', JSON.stringify(wh)).run();
|
|
662
|
+
} catch {}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
ctx.waitUntil(Promise.allSettled([
|
|
666
|
+
sendMetaCapi(env, 'Purchase', payload, request, ctx),
|
|
667
|
+
sendGA4Mp(env, 'purchase', payload, ctx),
|
|
668
|
+
sendTikTokApi(env, 'CompletePayment', payload, request, ctx),
|
|
669
|
+
sendPinterestCapi(env, 'Purchase', payload, request, ctx),
|
|
670
|
+
sendRedditCapi(env, 'Purchase', payload, request, ctx),
|
|
671
|
+
sendLinkedInCapi(env, 'Purchase', payload, request, ctx),
|
|
672
|
+
sendSpotifyCapi(env, 'Purchase', payload, request, ctx),
|
|
673
|
+
saveLead(env, 'Purchase', payload, request, 'ticto'),
|
|
674
|
+
sendWhatsApp(env, 'Purchase', payload),
|
|
675
|
+
]));
|
|
676
|
+
|
|
677
|
+
return new Response(JSON.stringify({ ok: true }), { status: 200, headers });
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// ── GET /webhook/whatsapp — verificação do webhook pela Meta ─────────────
|
|
681
|
+
if (request.method === 'GET' && url.pathname === '/webhook/whatsapp') {
|
|
682
|
+
const mode = url.searchParams.get('hub.mode');
|
|
683
|
+
const token = url.searchParams.get('hub.verify_token');
|
|
684
|
+
const challenge = url.searchParams.get('hub.challenge');
|
|
685
|
+
|
|
686
|
+
if (mode === 'subscribe' && env.WA_WEBHOOK_VERIFY_TOKEN && token === env.WA_WEBHOOK_VERIFY_TOKEN) {
|
|
687
|
+
return new Response(challenge, { status: 200, headers: { 'Content-Type': 'text/plain' } });
|
|
688
|
+
}
|
|
689
|
+
return new Response('Forbidden', { status: 403 });
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// ── POST /webhook/whatsapp — mensagens recebidas (CTWA) ──────────────────
|
|
693
|
+
if (request.method === 'POST' && url.pathname === '/webhook/whatsapp') {
|
|
694
|
+
let body;
|
|
695
|
+
try { body = await request.json(); } catch {
|
|
696
|
+
return new Response('JSON inválido', { status: 400 });
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const result = await processWhatsAppWebhook(env, body, request, ctx);
|
|
700
|
+
return new Response(JSON.stringify({ ok: true, ...result }), { status: 200, headers });
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// ── ML — Segmentação Dinâmica ─────────────────────────────────────────────
|
|
704
|
+
if (url.pathname === '/api/segmentation/cluster' && request.method === 'POST') {
|
|
705
|
+
return handleSegmentationCluster(env, request, headers);
|
|
706
|
+
}
|
|
707
|
+
if (url.pathname === '/api/segmentation/list' && request.method === 'GET') {
|
|
708
|
+
return handleSegmentationList(env, request, headers);
|
|
709
|
+
}
|
|
710
|
+
if (url.pathname === '/api/segmentation/outliers' && request.method === 'GET') {
|
|
711
|
+
return handleSegmentationOutliers(env, request, headers);
|
|
712
|
+
}
|
|
713
|
+
if (url.pathname === '/api/segmentation/update' && request.method === 'PUT') {
|
|
714
|
+
return handleSegmentationUpdate(env, request, headers);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// ── ML — Bidding Recommendations ──────────────────────────────────────────
|
|
718
|
+
if (url.pathname === '/api/bidding/recommend' && request.method === 'POST') {
|
|
719
|
+
return handleBiddingRecommend(env, request, headers);
|
|
720
|
+
}
|
|
721
|
+
if (url.pathname === '/api/bidding/history' && request.method === 'GET') {
|
|
722
|
+
return handleBiddingHistory(env, request, headers);
|
|
723
|
+
}
|
|
724
|
+
if (url.pathname === '/api/bidding/status' && request.method === 'GET') {
|
|
725
|
+
return handleBiddingStatus(env, request, headers);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// ── ML — A/B Testing de Prompts LTV ──────────────────────────────────────
|
|
729
|
+
if (url.pathname === '/api/ltv/ab-test/create' && request.method === 'POST') {
|
|
730
|
+
return handleLtvAbTestCreate(env, request, headers);
|
|
731
|
+
}
|
|
732
|
+
if (url.pathname === '/api/ltv/ab-test/list' && request.method === 'GET') {
|
|
733
|
+
return handleLtvAbTestList(env, request, headers);
|
|
734
|
+
}
|
|
735
|
+
if (url.pathname === '/api/ltv/ab-test/results' && request.method === 'GET') {
|
|
736
|
+
return handleLtvAbTestResults(env, request, headers);
|
|
737
|
+
}
|
|
738
|
+
if (url.pathname === '/api/ltv/ab-test/winner' && request.method === 'POST') {
|
|
739
|
+
return handleLtvAbTestWinner(env, request, headers);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// ── Fraud Detection — Fase 4 ──────────────────────────────────────────────
|
|
743
|
+
if (url.pathname === '/api/fraud/alerts' && request.method === 'GET') {
|
|
744
|
+
return handleFraudAlerts(env, request, headers);
|
|
745
|
+
}
|
|
746
|
+
if (url.pathname === '/api/fraud/blocklist' && request.method === 'GET') {
|
|
747
|
+
return handleFraudBlocklist(env, request, headers);
|
|
748
|
+
}
|
|
749
|
+
if (url.pathname === '/api/fraud/blocklist/add' && request.method === 'POST') {
|
|
750
|
+
return handleFraudBlocklistAdd(env, request, headers);
|
|
751
|
+
}
|
|
752
|
+
if (url.pathname === '/api/fraud/blocklist/remove' && request.method === 'DELETE') {
|
|
753
|
+
return handleFraudBlocklistRemove(env, request, headers);
|
|
754
|
+
}
|
|
755
|
+
if (url.pathname === '/api/fraud/stats' && request.method === 'GET') {
|
|
756
|
+
return handleFraudStats(env, request, headers);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// 404
|
|
760
|
+
return new Response(JSON.stringify({ error: 'rota não encontrada' }), { status: 404, headers });
|
|
761
|
+
},
|
|
762
|
+
|
|
763
|
+
// ── Cron Handler — Intelligence Agent ────────────────────────────────────────
|
|
764
|
+
async scheduled(event, env, ctx) {
|
|
765
|
+
const cron = event.cron;
|
|
766
|
+
const isMonthly = cron === '0 3 1 * *';
|
|
767
|
+
|
|
768
|
+
console.log(`[Intelligence Agent] Cron executado: ${cron}`);
|
|
769
|
+
ctx.waitUntil(runIntelligenceAgent(env, isMonthly ? 'monthly_audit' : 'weekly_check'));
|
|
770
|
+
},
|
|
771
|
+
|
|
772
|
+
// ── Queue Consumer — Retry de eventos com falha ───────────────────────────────
|
|
773
|
+
async queue(batch, env) {
|
|
774
|
+
for (const message of batch.messages) {
|
|
775
|
+
const { eventType, payload, platform, attempt = 1 } = message.body;
|
|
776
|
+
|
|
777
|
+
console.log(`[Queue] Reprocessando: ${platform}/${eventType} (tentativa ${attempt})`);
|
|
778
|
+
|
|
779
|
+
try {
|
|
780
|
+
if (platform === 'meta') await sendMetaCapi(env, eventType, payload, null, null);
|
|
781
|
+
if (platform === 'ga4') await sendGA4Mp(env, eventType, payload, null);
|
|
782
|
+
if (platform === 'tiktok') await sendTikTokApi(env, eventType, payload, null, null);
|
|
783
|
+
|
|
784
|
+
message.ack();
|
|
785
|
+
} catch (err) {
|
|
786
|
+
console.error(`[Queue] Falha ao reprocessar ${platform}/${eventType}:`, err.message);
|
|
787
|
+
message.retry();
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
},
|
|
791
|
+
};
|