cdp-edge 1.2.2 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/README.md +153 -306
  2. package/bin/cdp-edge.js +71 -61
  3. package/contracts/agent-versions.json +682 -0
  4. package/contracts/api-versions.json +372 -368
  5. package/contracts/types.ts +81 -0
  6. package/dist/commands/analyze.js +52 -52
  7. package/dist/commands/infra.js +54 -54
  8. package/dist/commands/install.js +26 -3
  9. package/dist/commands/server.js +174 -174
  10. package/dist/commands/setup.js +332 -100
  11. package/dist/commands/validate.js +248 -84
  12. package/dist/index.js +12 -12
  13. package/dist/sdk/cdpTrack.js +2095 -0
  14. package/dist/sdk/cdpTrack.min.js +64 -0
  15. package/dist/sdk/install-snippet.html +10 -0
  16. package/docs/whatsapp-ctwa.md +5 -4
  17. package/extracted-skill/tracking-events-generator/INTEGRACAO-COMPLETA.md +89 -0
  18. package/extracted-skill/tracking-events-generator/MELHORIAS-IMPLEMENTADAS.md +101 -0
  19. package/extracted-skill/tracking-events-generator/advanced-matching.js +364 -364
  20. package/extracted-skill/tracking-events-generator/agents/ab-ltv-agent.md +196 -0
  21. package/extracted-skill/tracking-events-generator/agents/ab-testing-agent.md +1 -1
  22. package/extracted-skill/tracking-events-generator/agents/attribution-agent.md +41 -41
  23. package/extracted-skill/tracking-events-generator/agents/bidding-agent.md +347 -0
  24. package/extracted-skill/tracking-events-generator/agents/bing-agent.md +40 -50
  25. package/extracted-skill/tracking-events-generator/agents/browser-tracking.md +174 -74
  26. package/extracted-skill/tracking-events-generator/agents/code-guardian-agent.md +1 -1
  27. package/extracted-skill/tracking-events-generator/agents/compliance-agent.md +25 -5
  28. package/extracted-skill/tracking-events-generator/agents/dashboard-agent.md +10 -10
  29. package/extracted-skill/tracking-events-generator/agents/database-agent.md +43 -42
  30. package/extracted-skill/tracking-events-generator/agents/debug-agent.md +22 -22
  31. package/extracted-skill/tracking-events-generator/agents/devops-agent.md +232 -0
  32. package/extracted-skill/tracking-events-generator/agents/domain-setup-agent.md +23 -9
  33. package/extracted-skill/tracking-events-generator/agents/email-agent.md +28 -1
  34. package/extracted-skill/tracking-events-generator/agents/evo-crm-agent.md +253 -0
  35. package/extracted-skill/tracking-events-generator/agents/fingerprint-agent.md +206 -1
  36. package/extracted-skill/tracking-events-generator/agents/fraud-detection-agent.md +143 -0
  37. package/extracted-skill/tracking-events-generator/agents/google-agent.md +128 -2
  38. package/extracted-skill/tracking-events-generator/agents/intelligence-agent.md +191 -31
  39. package/extracted-skill/tracking-events-generator/agents/lead-scoring-agent.md +282 -0
  40. package/extracted-skill/tracking-events-generator/agents/linkedin-agent.md +145 -34
  41. package/extracted-skill/tracking-events-generator/agents/localization-agent.md +1 -1
  42. package/extracted-skill/tracking-events-generator/agents/ltv-predictor-agent.md +24 -5
  43. package/extracted-skill/tracking-events-generator/agents/master-feedback-loop.md +81 -21
  44. package/extracted-skill/tracking-events-generator/agents/master-orchestrator.md +588 -93
  45. package/extracted-skill/tracking-events-generator/agents/match-quality-agent.md +304 -0
  46. package/extracted-skill/tracking-events-generator/agents/memory-agent.md +190 -15
  47. package/extracted-skill/tracking-events-generator/agents/meta-agent.md +10 -2
  48. package/extracted-skill/tracking-events-generator/agents/ml-clustering-agent.md +769 -0
  49. package/extracted-skill/tracking-events-generator/agents/page-analyzer.md +21 -4
  50. package/extracted-skill/tracking-events-generator/agents/performance-agent.md +41 -31
  51. package/extracted-skill/tracking-events-generator/agents/performance-optimization-agent.md +18 -8
  52. package/extracted-skill/tracking-events-generator/agents/pinterest-agent.md +14 -6
  53. package/extracted-skill/tracking-events-generator/agents/premium-tracking-intelligence-agent.md +7 -7
  54. package/extracted-skill/tracking-events-generator/agents/r2-setup-agent.md +16 -8
  55. package/extracted-skill/tracking-events-generator/agents/reddit-agent.md +15 -7
  56. package/extracted-skill/tracking-events-generator/agents/security-enterprise-agent.md +157 -48
  57. package/extracted-skill/tracking-events-generator/agents/server-tracking.md +35 -35
  58. package/extracted-skill/tracking-events-generator/agents/spotify-agent.md +15 -7
  59. package/extracted-skill/tracking-events-generator/agents/tiktok-agent.md +73 -2
  60. package/extracted-skill/tracking-events-generator/agents/tracking-plan-agent.md +104 -9
  61. package/extracted-skill/tracking-events-generator/agents/utm-agent.md +322 -0
  62. package/extracted-skill/tracking-events-generator/agents/validator-agent.md +13 -9
  63. package/extracted-skill/tracking-events-generator/agents/webhook-agent.md +112 -4
  64. package/extracted-skill/tracking-events-generator/agents/whatsapp-agent.md +58 -5
  65. package/extracted-skill/tracking-events-generator/agents/whatsapp-ctwa-setup-agent.md +26 -18
  66. package/extracted-skill/tracking-events-generator/agents/youtube-agent.md +152 -37
  67. package/extracted-skill/tracking-events-generator/anti-blocking.js +285 -285
  68. package/extracted-skill/tracking-events-generator/cdpTrack.js +642 -641
  69. package/extracted-skill/tracking-events-generator/contracts/api-versions.json +14 -10
  70. package/extracted-skill/tracking-events-generator/engagement-scoring.js +226 -226
  71. package/extracted-skill/tracking-events-generator/evals/evals.json +235 -235
  72. package/extracted-skill/tracking-events-generator/integration-test.js +497 -497
  73. package/extracted-skill/tracking-events-generator/knowledge-base.md +172 -0
  74. package/extracted-skill/tracking-events-generator/micro-events.js +992 -992
  75. package/extracted-skill/tracking-events-generator/models/lancamento-imobiliario.md +344 -0
  76. package/extracted-skill/tracking-events-generator/models/pinterest/conversions-api-template.js +144 -144
  77. package/extracted-skill/tracking-events-generator/models/pinterest/event-mappings.json +48 -48
  78. package/extracted-skill/tracking-events-generator/models/pinterest/tag-template.js +28 -28
  79. package/extracted-skill/tracking-events-generator/models/quiz-funnel.md +83 -19
  80. package/extracted-skill/tracking-events-generator/models/reddit/conversions-api-template.js +205 -205
  81. package/extracted-skill/tracking-events-generator/models/reddit/event-mappings.json +56 -56
  82. package/extracted-skill/tracking-events-generator/models/reddit/pixel-template.js +19 -19
  83. package/extracted-skill/tracking-events-generator/models/scenarios/behavior-engine.js +425 -425
  84. package/extracted-skill/tracking-events-generator/route-intent-capture.js +222 -0
  85. package/extracted-skill/tracking-events-generator/tracking.config.js +3 -3
  86. package/package.json +89 -75
  87. package/scripts/build-sdk.js +106 -0
  88. package/server-edge-tracker/.client.env.example +14 -0
  89. package/server-edge-tracker/INSTALAR.md +222 -23
  90. package/server-edge-tracker/SEGMENTATION-DOCS.md +513 -0
  91. package/server-edge-tracker/config/utm-mapping.json +64 -0
  92. package/server-edge-tracker/deploy-client.cjs +76 -0
  93. package/server-edge-tracker/index.ts +1230 -0
  94. package/server-edge-tracker/migrate-v7.sql +64 -0
  95. package/server-edge-tracker/modules/db.ts +710 -0
  96. package/server-edge-tracker/modules/dispatch/crm.ts +382 -0
  97. package/server-edge-tracker/modules/dispatch/ga4.ts +72 -0
  98. package/server-edge-tracker/modules/dispatch/meta.ts +143 -0
  99. package/server-edge-tracker/modules/dispatch/platforms.ts +255 -0
  100. package/server-edge-tracker/modules/dispatch/tiktok.ts +107 -0
  101. package/server-edge-tracker/modules/dispatch/whatsapp.ts +296 -0
  102. package/server-edge-tracker/modules/intelligence.ts +589 -0
  103. package/server-edge-tracker/modules/ml/bidding.ts +247 -0
  104. package/server-edge-tracker/modules/ml/fraud.ts +302 -0
  105. package/server-edge-tracker/modules/ml/logistic.ts +226 -0
  106. package/server-edge-tracker/modules/ml/ltv.ts +531 -0
  107. package/server-edge-tracker/modules/ml/matchquality.ts +232 -0
  108. package/server-edge-tracker/modules/ml/quiz.ts +343 -0
  109. package/server-edge-tracker/modules/ml/roas.ts +255 -0
  110. package/server-edge-tracker/modules/ml/segmentation.ts +407 -0
  111. package/server-edge-tracker/modules/nurture.ts +257 -0
  112. package/server-edge-tracker/modules/utils.ts +311 -0
  113. package/server-edge-tracker/modules/utm/utm-enricher.ts +231 -0
  114. package/server-edge-tracker/schema-ab-ltv.sql +97 -0
  115. package/server-edge-tracker/schema-bidding.sql +86 -0
  116. package/server-edge-tracker/schema-fraud.sql +90 -0
  117. package/server-edge-tracker/schema-indexes.sql +67 -0
  118. package/server-edge-tracker/schema-ltv-feedback.sql +11 -0
  119. package/server-edge-tracker/schema-quiz.sql +52 -0
  120. package/server-edge-tracker/schema-sales-engine.sql +113 -0
  121. package/server-edge-tracker/schema-segmentation.sql +219 -0
  122. package/server-edge-tracker/schema-utm.sql +82 -0
  123. package/server-edge-tracker/schema.sql +281 -265
  124. package/server-edge-tracker/types.ts +275 -0
  125. package/server-edge-tracker/wrangler.toml +140 -85
  126. package/templates/lancamento-imobiliario.md +344 -0
  127. package/templates/multi-step-checkout.md +3 -4
  128. package/templates/pinterest/conversions-api-template.js +144 -144
  129. package/templates/pinterest/event-mappings.json +48 -48
  130. package/templates/pinterest/tag-template.js +28 -28
  131. package/templates/quiz-funnel.md +83 -19
  132. package/templates/reddit/conversions-api-template.js +205 -205
  133. package/templates/reddit/event-mappings.json +56 -56
  134. package/templates/reddit/pixel-template.js +12 -39
  135. package/templates/scenarios/behavior-engine.js +45 -22
  136. package/docs/PixelBuilder-Documentacao-Completa (2).docx +0 -0
  137. package/docs/installation.md +0 -155
  138. package/docs/quick-start.md +0 -185
  139. package/extracted-skill/tracking-events-generator/agents/crm-integration-agent.md +0 -1419
  140. package/extracted-skill/tracking-events-generator/agents/intelligence-scheduling.md +0 -643
  141. package/server-edge-tracker/worker.js +0 -2574
@@ -0,0 +1,247 @@
1
+ /**
2
+ * CDP Edge — Bidding Recommendations ML (Fase 2)
3
+ * Handlers das rotas /api/bidding/*
4
+ */
5
+
6
+ import { tryParseJson } from '../utils.js';
7
+ import { Env } from '../../types.js';
8
+ import { ExecutionContext } from '@cloudflare/workers-types';
9
+
10
+ // ── Constantes de calibração ──────────────────────────────────────────────────
11
+ const PLATFORM_FACTORS: Record<string, number> = { meta: 0.85, google: 0.90, tiktok: 0.75 };
12
+
13
+ function getSegmentMultiplier(avgLtvClass: string | number, avgBehaviorScore: string | number): number {
14
+ const ltv = parseFloat(String(avgLtvClass) || '0');
15
+ const eng = parseFloat(String(avgBehaviorScore) || '0');
16
+ if (ltv >= 0.7 && eng >= 0.7) return 1.4;
17
+ if (ltv >= 0.7 && eng >= 0.4) return 1.2;
18
+ if (ltv >= 0.4 && eng >= 0.7) return 1.0;
19
+ if (ltv >= 0.4 && eng >= 0.4) return 0.8;
20
+ return 0.6;
21
+ }
22
+
23
+ function getConfidenceAdjustment(confidence: number): number {
24
+ if (confidence >= 0.7) return 1.00;
25
+ if (confidence >= 0.4) return 0.85;
26
+ return 0.70;
27
+ }
28
+
29
+ // ── POST /api/bidding/recommend ───────────────────────────────────────────────
30
+ export async function handleBiddingRecommend(env: Env, request: Request, headers: Headers): Promise<Response> {
31
+ if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
32
+
33
+ let body: any;
34
+ try { body = await request.json(); }
35
+ catch { return new Response(JSON.stringify({ error: 'JSON inválido no body' }), { status: 400, headers }); }
36
+
37
+ const {
38
+ vertical = 'geral', platform = 'meta',
39
+ target_roi = 3.5, period_days = 30,
40
+ campaign_id = null, budget = null,
41
+ } = body;
42
+
43
+ const platforms = platform === 'all'
44
+ ? Object.keys(PLATFORM_FACTORS)
45
+ : [platform].filter(p => PLATFORM_FACTORS[p]);
46
+
47
+ if (platforms.length === 0) {
48
+ return new Response(JSON.stringify({ error: 'platform deve ser: meta, google, tiktok ou all' }), { status: 400, headers });
49
+ }
50
+ if (target_roi < 1 || target_roi > 20) {
51
+ return new Response(JSON.stringify({ error: 'target_roi deve estar entre 1 e 20' }), { status: 400, headers });
52
+ }
53
+
54
+ try {
55
+ const segmentsRes = await env.DB.prepare(`
56
+ SELECT ms.id AS segment_id, ms.cluster_name, ms.avg_ltv_class, ms.avg_behavior_score,
57
+ ms.avg_engagement_score, ms.silhouette_score,
58
+ COUNT(msm.id) AS member_count,
59
+ AVG(l.predicted_ltv) AS real_avg_ltv,
60
+ SUM(CASE WHEN l.event_name IN ('Purchase','CompletePayment') THEN 1 ELSE 0 END) AS conversions
61
+ FROM ml_segments ms
62
+ LEFT JOIN ml_segment_members msm ON msm.cluster_id = ms.id
63
+ LEFT JOIN leads l ON CAST(msm.lead_id AS INTEGER) = l.id
64
+ AND l.created_at >= datetime('now', '-' || ? || ' days')
65
+ WHERE ms.is_active = 1 AND ms.client_vertical IN (?, 'general')
66
+ GROUP BY ms.id
67
+ HAVING member_count > 0
68
+ ORDER BY real_avg_ltv DESC
69
+ LIMIT 10
70
+ `).bind(period_days, vertical).all();
71
+
72
+ const segments = segmentsRes.results || [];
73
+
74
+ let globalLtv = 0, globalLeads = 0, globalConversions = 0;
75
+ if (segments.length === 0) {
76
+ const globalRes = await env.DB.prepare(`
77
+ SELECT COUNT(*) AS total_leads, AVG(predicted_ltv) AS avg_ltv,
78
+ SUM(CASE WHEN event_name IN ('Purchase','CompletePayment') THEN 1 ELSE 0 END) AS conversions
79
+ FROM leads
80
+ WHERE created_at >= datetime('now', '-' || ? || ' days')
81
+ AND (bot_score IS NULL OR bot_score < 2)
82
+ `).bind(period_days).first();
83
+
84
+ globalLeads = Number((globalRes as any)?.total_leads) || 0;
85
+ globalLtv = Number((globalRes as any)?.avg_ltv) || 0;
86
+ globalConversions = Number((globalRes as any)?.conversions) || 0;
87
+
88
+ if (globalLeads < 10) {
89
+ return new Response(JSON.stringify({
90
+ error: `Dados insuficientes. Apenas ${globalLeads} leads no período de ${period_days} dias. Mínimo: 10.`,
91
+ leads_found: globalLeads, required: 10,
92
+ }), { status: 400, headers });
93
+ }
94
+ }
95
+
96
+ const now = new Date().toISOString();
97
+ const recommendations: any[] = [];
98
+
99
+ const targetSegments = segments.length > 0
100
+ ? segments
101
+ : [{ segment_id: null, cluster_name: 'Global (sem segmentação)', avg_ltv_class: 0.5, avg_behavior_score: 0.5, avg_engagement_score: 0.5, member_count: globalLeads, real_avg_ltv: globalLtv, conversions: globalConversions }];
102
+
103
+ for (const seg of targetSegments) {
104
+ const avgLtv = parseFloat(String(seg.real_avg_ltv) || '0');
105
+ const convs = parseInt(String(seg.conversions || '0'));
106
+ const confidence = Math.min(1, convs / 100);
107
+
108
+ const estimatedLtv = avgLtv > 0 ? avgLtv :
109
+ Number(seg.avg_ltv_class) >= 0.7 ? 497 : Number(seg.avg_ltv_class) >= 0.4 ? 297 : 97;
110
+
111
+ const cpaTarget = estimatedLtv / target_roi;
112
+ const segMult = getSegmentMultiplier(String(seg.avg_ltv_class), String(seg.avg_behavior_score));
113
+ const confAdj = getConfidenceAdjustment(confidence);
114
+ const alertMsg = convs < 30 ? `Atenção: apenas ${convs} conversões no período. Bid baseado em estimativa de LTV — aplique com cautela.` : null;
115
+
116
+ for (const plat of platforms) {
117
+ const platFactor = PLATFORM_FACTORS[plat] || 0.8;
118
+ const recommendedBid = Math.max(5, cpaTarget * platFactor * segMult * confAdj);
119
+ const expectedRoi = estimatedLtv / (recommendedBid / platFactor);
120
+
121
+ const reasoning = `Segmento "${seg.cluster_name}": LTV=${estimatedLtv.toFixed(0)} BRL, ` +
122
+ `CPA_alvo=${cpaTarget.toFixed(0)} BRL, fator_plataforma=${platFactor}, ` +
123
+ `mult_segmento=${segMult}, ajuste_confiança=${confAdj}, ` +
124
+ `base: ${convs} conversões em ${period_days} dias.`;
125
+
126
+ try {
127
+ await env.DB.prepare(`
128
+ INSERT INTO bid_recommendations (
129
+ generated_at, vertical, platform, period_days, target_roi,
130
+ segment_id, segment_name, leads_analyzed, conversions_found,
131
+ avg_ltv, cpa_target, recommended_bid, bid_currency,
132
+ confidence, expected_roi, reasoning, ai_used, alert_message,
133
+ platform_factor, confidence_adjustment, segment_multiplier, is_active
134
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,0,?,?,?,?,1)
135
+ `).bind(
136
+ now, vertical, plat, period_days, target_roi,
137
+ seg.segment_id || null, seg.cluster_name,
138
+ seg.member_count || globalLeads, convs,
139
+ estimatedLtv, cpaTarget, recommendedBid, 'BRL',
140
+ confidence, expectedRoi, reasoning, alertMsg || null,
141
+ platFactor, confAdj, segMult,
142
+ ).run();
143
+ } catch (e: any) { console.error('[Bidding] D1 insert error:', e?.message || String(e)); }
144
+
145
+ recommendations.push({
146
+ platform: plat, segment: seg.cluster_name, segment_id: seg.segment_id || null,
147
+ avg_ltv: Math.round(estimatedLtv * 100) / 100,
148
+ avg_ltv_class: Number(seg.avg_ltv_class) >= 0.7 ? 'High' : Number(seg.avg_ltv_class) >= 0.4 ? 'Medium' : 'Low',
149
+ cpa_target: Math.round(cpaTarget * 100) / 100,
150
+ recommended_bid: Math.round(recommendedBid * 100) / 100,
151
+ bid_currency: 'BRL', confidence: Math.round(confidence * 100) / 100,
152
+ expected_roi: Math.round(expectedRoi * 100) / 100, reasoning, alert: alertMsg,
153
+ });
154
+ }
155
+ }
156
+
157
+ await env.DB.prepare(`UPDATE bid_recommendations SET is_active = 0 WHERE vertical = ? AND generated_at < ? AND is_active = 1`).bind(vertical, now).run().catch(() => {});
158
+
159
+ const avgConfidence = recommendations.length > 0
160
+ ? recommendations.reduce((s, r) => s + r.confidence, 0) / recommendations.length
161
+ : 0;
162
+
163
+ return new Response(JSON.stringify({
164
+ success: true, generated_at: now, vertical, period_days, target_roi,
165
+ data_quality: {
166
+ leads_analyzed: targetSegments.reduce((s: number, sg: any) => s + (sg.member_count || 0), 0),
167
+ conversions_found: targetSegments.reduce((s: number, sg: any) => s + (sg.conversions || 0), 0),
168
+ segments_active: segments.length, confidence: Math.round(avgConfidence * 100) / 100,
169
+ },
170
+ recommendations,
171
+ global_summary: {
172
+ total_recommendations: recommendations.length,
173
+ avg_confidence: Math.round(avgConfidence * 100) / 100,
174
+ expected_cost_reduction: avgConfidence >= 0.7 ? '-20%' : avgConfidence >= 0.4 ? '-10%' : 'indefinido (dados insuficientes)',
175
+ segments_analyzed: segments.length,
176
+ },
177
+ }), { status: 200, headers });
178
+
179
+ } catch (err: any) {
180
+ console.error('[Bidding] recommend error:', err?.message || String(err));
181
+ return new Response(JSON.stringify({ error: 'Erro ao gerar recomendações', message: err?.message || String(err) }), { status: 500, headers });
182
+ }
183
+ }
184
+
185
+ // ── GET /api/bidding/history ──────────────────────────────────────────────────
186
+ export async function handleBiddingHistory(env: Env, request: Request, headers: Headers): Promise<Response> {
187
+ if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
188
+
189
+ const url = new URL(request.url);
190
+ const vertical = url.searchParams.get('vertical') || null;
191
+ const platform = url.searchParams.get('platform') || null;
192
+ const limit = Math.min(parseInt(url.searchParams.get('limit') || '20'), 100);
193
+
194
+ try {
195
+ const conditions: string[] = [];
196
+ const bindings: (string | number)[] = [];
197
+ if (vertical) { conditions.push('vertical = ?'); bindings.push(vertical); }
198
+ if (platform) { conditions.push('platform = ?'); bindings.push(platform); }
199
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
200
+
201
+ const result = await env.DB.prepare(`
202
+ SELECT id, generated_at, vertical, platform, period_days, target_roi,
203
+ segment_name, leads_analyzed, conversions_found, avg_ltv, cpa_target,
204
+ recommended_bid, bid_currency, confidence, expected_roi,
205
+ reasoning, alert_message, ai_used, is_active,
206
+ applied_at, applied_campaign, applied_result
207
+ FROM bid_recommendations
208
+ ${where}
209
+ ORDER BY generated_at DESC
210
+ LIMIT ?
211
+ `).bind(...bindings, limit).all();
212
+
213
+ const items = (result.results || []).map((r: any) => ({ ...r, applied_result: tryParseJson(r.applied_result, null) }));
214
+ return new Response(JSON.stringify({ success: true, total: items.length, history: items }), { status: 200, headers });
215
+ } catch (err: any) {
216
+ console.error('[Bidding] history error:', err?.message || String(err));
217
+ return new Response(JSON.stringify({ error: err?.message || String(err) }), { status: 500, headers });
218
+ }
219
+ }
220
+
221
+ // ── GET /api/bidding/status ───────────────────────────────────────────────────
222
+ export async function handleBiddingStatus(env: Env, request: Request, headers: Headers): Promise<Response> {
223
+ if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
224
+
225
+ const url = new URL(request.url);
226
+ const vertical = url.searchParams.get('vertical') || null;
227
+
228
+ try {
229
+ let query = `
230
+ SELECT platform, vertical, MAX(generated_at) as last_generated,
231
+ AVG(confidence) as avg_confidence, AVG(recommended_bid) as avg_bid,
232
+ COUNT(*) as recommendations_count,
233
+ SUM(CASE WHEN alert_message IS NOT NULL THEN 1 ELSE 0 END) as alerts_count
234
+ FROM bid_recommendations
235
+ WHERE is_active = 1
236
+ `;
237
+ const bindings: (string | number)[] = [];
238
+ if (vertical) { query += ' AND vertical = ?'; bindings.push(vertical); }
239
+ query += ' GROUP BY platform, vertical ORDER BY last_generated DESC';
240
+
241
+ const result = await env.DB.prepare(query).bind(...bindings).all();
242
+ return new Response(JSON.stringify({ success: true, status: result.results || [] }), { status: 200, headers });
243
+ } catch (err: any) {
244
+ console.error('[Bidding] status error:', err?.message || String(err));
245
+ return new Response(JSON.stringify({ error: err?.message || String(err) }), { status: 500, headers });
246
+ }
247
+ }
@@ -0,0 +1,302 @@
1
+ /**
2
+ * CDP Edge — Fraud Detection (Fase 4)
3
+ * checkFraudGate, logFraudSignal, handlers das rotas /api/fraud/*
4
+ */
5
+
6
+ import { sha256, tryParseJson } from '../utils.js';
7
+ import { Env, TrackPayload } from '../../types.js';
8
+
9
+ // ── Tipos ───────────────────────────────────────────────────────────────────────
10
+ export interface FraudResult {
11
+ allowed: boolean;
12
+ score: number;
13
+ reasons: string[];
14
+ action: 'allowed' | 'flagged' | 'dropped';
15
+ }
16
+
17
+ export const DATACENTER_PATTERNS = /amazon|google|microsoft|digitalocean|linode|ovh|vultr|hetzner|contabo|cloudflare|packet|rackspace|leaseweb/i;
18
+
19
+ // ── checkFraudGate — roda ANTES de qualquer processamento de evento ────────────
20
+ // Retorna { allowed, score, reasons, action }
21
+ // Falhas no gate = fail-safe (deixa passar)
22
+ export async function checkFraudGate(env: Env, request: Request, payload: TrackPayload): Promise<FraudResult> {
23
+ const result: FraudResult = { allowed: true, score: 0, reasons: [], action: 'allowed' };
24
+
25
+ try {
26
+ const ip = request.headers.get('CF-Connecting-IP') || '';
27
+ const ua = request.headers.get('User-Agent') || '';
28
+ const fingerprint = (payload as any).fingerprint || '';
29
+ const email = payload.email || '';
30
+ const botScore = parseInt(String(payload.botScore || (payload as any).bot_score || 0));
31
+ const asn = String((request as any).cf?.asOrganization || '').toLowerCase();
32
+ const country = ((request as any).cf?.country || '').toUpperCase();
33
+ const acceptLang = request.headers.get('Accept-Language');
34
+
35
+ // 1. KV blocklist check — instantâneo (~0ms)
36
+ if (env.GEO_CACHE && ip) {
37
+ const ipBlocked = await env.GEO_CACHE.get(`fraud_block:ip:${ip}`);
38
+ if (ipBlocked) {
39
+ return { allowed: false, score: 100, reasons: ['ip_blocklisted'], action: 'dropped' };
40
+ }
41
+ }
42
+ if (env.GEO_CACHE && fingerprint) {
43
+ const fpBlocked = await env.GEO_CACHE.get(`fraud_block:fp:${fingerprint}`);
44
+ if (fpBlocked) {
45
+ return { allowed: false, score: 100, reasons: ['fingerprint_blocklisted'], action: 'dropped' };
46
+ }
47
+ }
48
+
49
+ // 2. Bot score
50
+ if (botScore >= 3) { result.score += 60; result.reasons.push('bot_score_high'); }
51
+ else if (botScore === 2) { result.score += 30; result.reasons.push('bot_score_medium'); }
52
+
53
+ // 3. User-Agent suspeito
54
+ if (/headless|phantomjs|selenium|webdriver|curl|python|scrapy|bot|crawler|spider/i.test(ua)) {
55
+ result.score += 40; result.reasons.push('suspicious_user_agent');
56
+ }
57
+
58
+ // 4. Datacenter IP
59
+ if (ip && DATACENTER_PATTERNS.test(asn)) {
60
+ result.score += 35; result.reasons.push('datacenter_ip');
61
+ }
62
+
63
+ // 5. Sem Accept-Language
64
+ if (!acceptLang) {
65
+ result.score += 20; result.reasons.push('no_accept_language');
66
+ }
67
+
68
+ // 6. Velocity check via KV
69
+ if (env.GEO_CACHE && ip) {
70
+ const velKey1h = `fraud_velocity:${ip}:h`;
71
+ const velStr = await env.GEO_CACHE.get(velKey1h);
72
+ const vel1h = parseInt(velStr || '0') + 1;
73
+
74
+ await env.GEO_CACHE.put(velKey1h, String(vel1h), { expirationTtl: 3600 });
75
+
76
+ if (vel1h > 20) { result.score += 50; result.reasons.push('ip_velocity_very_high'); }
77
+ else if (vel1h > 10) { result.score += 25; result.reasons.push('ip_velocity_high'); }
78
+ }
79
+
80
+ result.score = Math.min(100, result.score);
81
+
82
+ // 8. Decisão final
83
+ if (result.score >= 80) {
84
+ result.allowed = false;
85
+ result.action = 'dropped';
86
+ } else if (result.score >= 40) {
87
+ result.action = 'flagged';
88
+ }
89
+
90
+ return result;
91
+
92
+ } catch (err: any) {
93
+ console.error('[Fraud] checkFraudGate error:', err?.message || String(err));
94
+ return { allowed: true, score: 0, reasons: ['gate_error_fallback'], action: 'allowed' };
95
+ }
96
+ }
97
+
98
+ // ── logFraudSignal — persiste no D1 em background ────────────────────────────
99
+ export async function logFraudSignal(env: Env, request: Request, payload: TrackPayload, fraudResult: FraudResult): Promise<void> {
100
+ if (!env.DB || fraudResult.action === 'allowed') return;
101
+ try {
102
+ const ip = request.headers.get('CF-Connecting-IP') || '';
103
+ const ua = request.headers.get('User-Agent') || '';
104
+ const fingerprint = (payload as any).fingerprint || '';
105
+ const botScore = parseInt(String(payload.botScore || (payload as any).bot_score || 0));
106
+ const asn = String((request as any).cf?.asOrganization || '');
107
+ const country = (request as any).cf?.country || '';
108
+ const velKey1h = `fraud_velocity:${ip}:h`;
109
+ const vel1h = env.GEO_CACHE ? parseInt(await env.GEO_CACHE.get(velKey1h) || '0') : 0;
110
+
111
+ let emailHash = null;
112
+ if (payload.email) {
113
+ try {
114
+ emailHash = await sha256(payload.email.trim().toLowerCase());
115
+ } catch (err: any) {
116
+ console.error('[Fraud] Error generating email hash:', {
117
+ email: payload.email,
118
+ error: err?.message || String(err),
119
+ stack: err?.stack,
120
+ });
121
+ }
122
+ }
123
+
124
+ await env.DB.prepare(`
125
+ INSERT INTO fraud_signals (
126
+ ip_address, fingerprint, user_id, email_hash, event_name, event_id,
127
+ fraud_score, action_taken, reasons,
128
+ ip_country, ip_asn, user_agent, bot_score, velocity_1h, detected_at
129
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,datetime('now'))
130
+ `).bind(
131
+ ip, fingerprint || null, payload.userId || null, emailHash,
132
+ payload.eventName || null, payload.eventId || null,
133
+ fraudResult.score, fraudResult.action, JSON.stringify(fraudResult.reasons),
134
+ country, asn, ua.substring(0, 255), botScore, vel1h,
135
+ ).run();
136
+
137
+ if (fraudResult.action === 'dropped' && ip) {
138
+ await env.DB.prepare(`
139
+ INSERT INTO fraud_alerts (alert_type, entity_type, entity_value, events_total, events_dropped, peak_score, first_seen, last_seen, top_reasons)
140
+ VALUES ('ip_attack', 'ip', ?, 1, 1, ?, datetime('now'), datetime('now'), ?)
141
+ ON CONFLICT(entity_type, entity_value) DO UPDATE SET
142
+ events_total = events_total + 1,
143
+ events_dropped = events_dropped + 1,
144
+ peak_score = MAX(peak_score, excluded.peak_score),
145
+ last_seen = datetime('now'),
146
+ updated_at = datetime('now')
147
+ `).bind(ip, fraudResult.score, JSON.stringify(fraudResult.reasons)).run().catch(() => {});
148
+ }
149
+ } catch (err: any) {
150
+ console.error('[Fraud] logFraudSignal error:', err?.message || String(err));
151
+ }
152
+ }
153
+
154
+ // ── GET /api/fraud/alerts ─────────────────────────────────────────────────────
155
+ export async function handleFraudAlerts(env: Env, request: Request, headers: Headers): Promise<Response> {
156
+ if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
157
+
158
+ const url = new URL(request.url);
159
+ const action = url.searchParams.get('action') || null;
160
+ const hours = parseInt(url.searchParams.get('hours') || '24');
161
+ const limit = Math.min(parseInt(url.searchParams.get('limit') || '50'), 200);
162
+
163
+ try {
164
+ const cond = action ? 'AND action_taken = ?' : '';
165
+ const bindings = action ? [hours, action, limit] : [hours, limit];
166
+
167
+ const result = await env.DB.prepare(`
168
+ SELECT ip_address, fingerprint, event_name, fraud_score, action_taken,
169
+ reasons, ip_country, ip_asn, bot_score, velocity_1h, detected_at
170
+ FROM fraud_signals
171
+ WHERE detected_at >= datetime('now', '-' || ? || ' hours')
172
+ ${cond}
173
+ ORDER BY fraud_score DESC, detected_at DESC
174
+ LIMIT ?
175
+ `).bind(...bindings).all();
176
+
177
+ const signals = (result.results || []).map((s: any) => ({ ...s, reasons: tryParseJson(s.reasons, []) }));
178
+ const stats = await env.DB.prepare(`SELECT * FROM v_fraud_dashboard`).first().catch(() => null);
179
+
180
+ return new Response(JSON.stringify({ success: true, period_hours: hours, total: signals.length, stats, alerts: signals }), { status: 200, headers });
181
+ } catch (err: any) {
182
+ console.error('[Fraud] alerts error:', err?.message || String(err));
183
+ return new Response(JSON.stringify({ error: err?.message || String(err) }), { status: 500, headers });
184
+ }
185
+ }
186
+
187
+ // ── GET /api/fraud/blocklist ──────────────────────────────────────────────────
188
+ export async function handleFraudBlocklist(env: Env, request: Request, headers: Headers): Promise<Response> {
189
+ if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
190
+
191
+ try {
192
+ const result = await env.DB.prepare(`
193
+ SELECT entity_type, entity_value, events_total, events_dropped,
194
+ peak_score, first_seen, last_seen, blocked_at, block_expires, top_reasons
195
+ FROM fraud_alerts WHERE is_blocked = 1 ORDER BY events_dropped DESC LIMIT 100
196
+ `).all();
197
+
198
+ const blocklist = (result.results || []).map((r: any) => ({ ...r, top_reasons: tryParseJson(r.top_reasons, []) }));
199
+ return new Response(JSON.stringify({ success: true, total: blocklist.length, blocklist }), { status: 200, headers });
200
+ } catch (err: any) {
201
+ console.error('[Fraud] blocklist error:', err?.message || String(err));
202
+ return new Response(JSON.stringify({ error: err?.message || String(err) }), { status: 500, headers });
203
+ }
204
+ }
205
+
206
+ // ── POST /api/fraud/blocklist/add ─────────────────────────────────────────────
207
+ export async function handleFraudBlocklistAdd(env: Env, request: Request, headers: Headers): Promise<Response> {
208
+ if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
209
+
210
+ let body: any;
211
+ try { body = await request.json(); }
212
+ catch { return new Response(JSON.stringify({ error: 'JSON inválido' }), { status: 400, headers }); }
213
+
214
+ const { entity_type, entity_value, ttl_hours = 24, reason = 'manual_block' } = body;
215
+ if (!entity_type || !entity_value) {
216
+ return new Response(JSON.stringify({ error: 'entity_type (ip|fingerprint) e entity_value são obrigatórios' }), { status: 400, headers });
217
+ }
218
+ if (!['ip', 'fingerprint'].includes(entity_type)) {
219
+ return new Response(JSON.stringify({ error: 'entity_type deve ser: ip ou fingerprint' }), { status: 400, headers });
220
+ }
221
+
222
+ try {
223
+ const kvKey = `fraud_block:${entity_type}:${entity_value}`;
224
+ const ttlSec = Math.min(ttl_hours * 3600, 7 * 24 * 3600);
225
+ const expiresAt = new Date(Date.now() + ttlSec * 1000).toISOString();
226
+
227
+ if (env.GEO_CACHE) {
228
+ await env.GEO_CACHE.put(kvKey, JSON.stringify({ reason, blocked_at: new Date().toISOString() }), { expirationTtl: ttlSec });
229
+ }
230
+
231
+ await env.DB.prepare(`
232
+ INSERT INTO fraud_alerts (alert_type, entity_type, entity_value, events_total, events_dropped, peak_score, first_seen, last_seen, is_blocked, blocked_at, block_expires, top_reasons)
233
+ VALUES ('manual', ?, ?, 0, 0, 100, datetime('now'), datetime('now'), 1, datetime('now'), ?, ?)
234
+ ON CONFLICT DO UPDATE SET is_blocked = 1, blocked_at = datetime('now'), block_expires = excluded.block_expires, updated_at = datetime('now')
235
+ `).bind(entity_type, entity_value, expiresAt, JSON.stringify([reason])).run().catch(() => {});
236
+
237
+ return new Response(JSON.stringify({
238
+ success: true, entity_type, entity_value, kv_key: kvKey, ttl_hours, expires_at: expiresAt,
239
+ message: `${entity_type} '${entity_value}' bloqueado por ${ttl_hours}h. Efeito imediato via KV.`,
240
+ }), { status: 200, headers });
241
+ } catch (err: any) {
242
+ console.error('[Fraud] blocklist add error:', err?.message || String(err));
243
+ return new Response(JSON.stringify({ error: err?.message || String(err) }), { status: 500, headers });
244
+ }
245
+ }
246
+
247
+ // ── DELETE /api/fraud/blocklist/remove ───────────────────────────────────────
248
+ export async function handleFraudBlocklistRemove(env: Env, request: Request, headers: Headers): Promise<Response> {
249
+ if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
250
+
251
+ let body: any;
252
+ try { body = await request.json(); }
253
+ catch { return new Response(JSON.stringify({ error: 'JSON inválido' }), { status: 400, headers }); }
254
+
255
+ const { entity_type, entity_value } = body;
256
+ if (!entity_type || !entity_value) {
257
+ return new Response(JSON.stringify({ error: 'entity_type e entity_value são obrigatórios' }), { status: 400, headers });
258
+ }
259
+
260
+ try {
261
+ const kvKey = `fraud_block:${entity_type}:${entity_value}`;
262
+ if (env.GEO_CACHE) await env.GEO_CACHE.delete(kvKey);
263
+ await env.DB.prepare(`UPDATE fraud_alerts SET is_blocked = 0, resolved_at = datetime('now'), resolved_by = 'manual' WHERE entity_type = ? AND entity_value = ?`).bind(entity_type, entity_value).run();
264
+
265
+ return new Response(JSON.stringify({
266
+ success: true, entity_type, entity_value,
267
+ message: `${entity_type} '${entity_value}' removido do blocklist. Efeito imediato via KV.`,
268
+ }), { status: 200, headers });
269
+ } catch (err: any) {
270
+ console.error('[Fraud] blocklist remove error:', err?.message || String(err));
271
+ return new Response(JSON.stringify({ error: err?.message || String(err) }), { status: 500, headers });
272
+ }
273
+ }
274
+
275
+ // ── GET /api/fraud/stats ──────────────────────────────────────────────────────
276
+ export async function handleFraudStats(env: Env, request: Request, headers: Headers): Promise<Response> {
277
+ if (!env.DB) return new Response(JSON.stringify({ error: 'DB não configurado' }), { status: 503, headers });
278
+
279
+ try {
280
+ const dashboard = await env.DB.prepare(`SELECT * FROM v_fraud_dashboard`).first();
281
+ const topIps = await env.DB.prepare(`
282
+ SELECT ip_address, COUNT(*) as events, MAX(fraud_score) as peak_score
283
+ FROM fraud_signals
284
+ WHERE detected_at >= datetime('now', '-24 hours') AND action_taken = 'dropped'
285
+ GROUP BY ip_address ORDER BY events DESC LIMIT 10
286
+ `).all();
287
+ const topReasons = await env.DB.prepare(`
288
+ SELECT action_taken, COUNT(*) as count FROM fraud_signals
289
+ WHERE detected_at >= datetime('now', '-24 hours')
290
+ GROUP BY action_taken
291
+ `).all();
292
+
293
+ return new Response(JSON.stringify({
294
+ success: true, period: '24h', dashboard,
295
+ top_attacking_ips: topIps.results || [],
296
+ by_action: topReasons.results || [],
297
+ }), { status: 200, headers });
298
+ } catch (err: any) {
299
+ console.error('[Fraud] stats error:', err?.message || String(err));
300
+ return new Response(JSON.stringify({ error: err?.message || String(err) }), { status: 500, headers });
301
+ }
302
+ }