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