cdp-edge 2.0.0 → 2.0.2
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/api-versions.json +12 -8
- package/dist/commands/install.js +1 -2
- package/dist/commands/setup.js +1 -2
- package/extracted-skill/tracking-events-generator/agents/attribution-agent.md +23 -23
- package/extracted-skill/tracking-events-generator/agents/browser-tracking.md +172 -72
- package/extracted-skill/tracking-events-generator/agents/compliance-agent.md +20 -0
- package/extracted-skill/tracking-events-generator/agents/crm-integration-agent.md +48 -16
- package/extracted-skill/tracking-events-generator/agents/dashboard-agent.md +7 -7
- package/extracted-skill/tracking-events-generator/agents/database-agent.md +8 -8
- package/extracted-skill/tracking-events-generator/agents/debug-agent.md +13 -13
- package/extracted-skill/tracking-events-generator/agents/devops-agent.md +31 -7
- package/extracted-skill/tracking-events-generator/agents/email-agent.md +27 -0
- package/extracted-skill/tracking-events-generator/agents/fingerprint-agent.md +205 -0
- package/extracted-skill/tracking-events-generator/agents/google-agent.md +118 -0
- package/extracted-skill/tracking-events-generator/agents/intelligence-agent.md +90 -4
- package/extracted-skill/tracking-events-generator/agents/intelligence-scheduling.md +8 -641
- package/extracted-skill/tracking-events-generator/agents/linkedin-agent.md +108 -0
- package/extracted-skill/tracking-events-generator/agents/ltv-predictor-agent.md +1 -1
- package/extracted-skill/tracking-events-generator/agents/master-feedback-loop.md +68 -8
- package/extracted-skill/tracking-events-generator/agents/master-orchestrator.md +61 -18
- package/extracted-skill/tracking-events-generator/agents/memory-agent.md +98 -0
- package/extracted-skill/tracking-events-generator/agents/performance-agent.md +29 -19
- package/extracted-skill/tracking-events-generator/agents/performance-optimization-agent.md +11 -1
- package/extracted-skill/tracking-events-generator/agents/security-enterprise-agent.md +137 -28
- package/extracted-skill/tracking-events-generator/agents/server-tracking.md +7 -8
- package/extracted-skill/tracking-events-generator/agents/tiktok-agent.md +63 -0
- package/extracted-skill/tracking-events-generator/agents/tracking-plan-agent.md +100 -5
- package/extracted-skill/tracking-events-generator/agents/webhook-agent.md +100 -0
- package/extracted-skill/tracking-events-generator/agents/whatsapp-agent.md +58 -5
- package/extracted-skill/tracking-events-generator/agents/whatsapp-ctwa-setup-agent.md +16 -16
- package/extracted-skill/tracking-events-generator/agents/youtube-agent.md +140 -25
- package/extracted-skill/tracking-events-generator/contracts/api-versions.json +12 -8
- package/package.json +2 -2
- package/server-edge-tracker/worker.js +53 -8
|
@@ -402,7 +402,7 @@ export async function checkIPBlocking(request, env) {
|
|
|
402
402
|
const ip = request.headers.get('CF-Connecting-IP') || 'unknown';
|
|
403
403
|
|
|
404
404
|
// 1. Verificar whitelist (primeiro - bypass rate limiting)
|
|
405
|
-
const isWhitelisted = await checkIPWhitelist(ip);
|
|
405
|
+
const isWhitelisted = await checkIPWhitelist(ip, env);
|
|
406
406
|
if (isWhitelisted) {
|
|
407
407
|
return {
|
|
408
408
|
allowed: true,
|
|
@@ -412,7 +412,7 @@ export async function checkIPBlocking(request, env) {
|
|
|
412
412
|
}
|
|
413
413
|
|
|
414
414
|
// 2. Verificar blacklist manual
|
|
415
|
-
const isManuallyBlocked = await checkIPBlacklist(ip);
|
|
415
|
+
const isManuallyBlocked = await checkIPBlacklist(ip, env);
|
|
416
416
|
if (isManuallyBlocked) {
|
|
417
417
|
await logSecurityEvent({
|
|
418
418
|
type: 'IP_BLACKLISTED',
|
|
@@ -430,7 +430,7 @@ export async function checkIPBlocking(request, env) {
|
|
|
430
430
|
}
|
|
431
431
|
|
|
432
432
|
// 3. Verificar geoblocking
|
|
433
|
-
const isGeoBlocked = await checkGeoBlocking(request);
|
|
433
|
+
const isGeoBlocked = await checkGeoBlocking(request, env);
|
|
434
434
|
if (isGeoBlocked) {
|
|
435
435
|
await logSecurityEvent({
|
|
436
436
|
type: 'IP_GEO_BLOCKED',
|
|
@@ -447,7 +447,7 @@ export async function checkIPBlocking(request, env) {
|
|
|
447
447
|
}
|
|
448
448
|
|
|
449
449
|
// 4. Verificar bloqueio automático (comportamento malicioso)
|
|
450
|
-
const isAutoBlocked = await checkAutoIPBlocking(ip);
|
|
450
|
+
const isAutoBlocked = await checkAutoIPBlocking(ip, env);
|
|
451
451
|
if (isAutoBlocked) {
|
|
452
452
|
await logSecurityEvent({
|
|
453
453
|
type: 'IP_AUTO_BLOCKED',
|
|
@@ -470,13 +470,13 @@ export async function checkIPBlocking(request, env) {
|
|
|
470
470
|
}
|
|
471
471
|
|
|
472
472
|
// Verificar se IP está na whitelist
|
|
473
|
-
async function checkIPWhitelist(ip) {
|
|
473
|
+
async function checkIPWhitelist(ip, env) {
|
|
474
474
|
if (!IP_BLOCKING_CONFIG.whitelist.enabled) {
|
|
475
475
|
return false;
|
|
476
476
|
}
|
|
477
477
|
|
|
478
478
|
// 1. Verificar whitelist manual (IP exato)
|
|
479
|
-
const manualWhitelist = await DB.prepare(`
|
|
479
|
+
const manualWhitelist = await env.DB.prepare(`
|
|
480
480
|
SELECT ip, cidr_range
|
|
481
481
|
FROM ip_whitelist
|
|
482
482
|
WHERE ip = ?
|
|
@@ -497,8 +497,8 @@ async function checkIPWhitelist(ip) {
|
|
|
497
497
|
}
|
|
498
498
|
|
|
499
499
|
// Verificar se IP está na blacklist
|
|
500
|
-
async function checkIPBlacklist(ip) {
|
|
501
|
-
const blocked = await DB.prepare(`
|
|
500
|
+
async function checkIPBlacklist(ip, env) {
|
|
501
|
+
const blocked = await env.DB.prepare(`
|
|
502
502
|
SELECT block_reason, blocked_at, unblocked_at, blocking_type, violation_count
|
|
503
503
|
FROM ip_blacklist
|
|
504
504
|
WHERE ip = ? AND unblocked_at IS NULL
|
|
@@ -508,7 +508,7 @@ async function checkIPBlacklist(ip) {
|
|
|
508
508
|
}
|
|
509
509
|
|
|
510
510
|
// Verificar geoblocking
|
|
511
|
-
async function checkGeoBlocking(request) {
|
|
511
|
+
async function checkGeoBlocking(request, env) {
|
|
512
512
|
if (!IP_BLOCKING_CONFIG.blacklist.geo_blocking.enabled) {
|
|
513
513
|
return null;
|
|
514
514
|
}
|
|
@@ -527,7 +527,7 @@ async function checkGeoBlocking(request) {
|
|
|
527
527
|
}
|
|
528
528
|
|
|
529
529
|
// Verificar bloqueio automático por comportamento
|
|
530
|
-
async function checkAutoIPBlocking(ip) {
|
|
530
|
+
async function checkAutoIPBlocking(ip, env) {
|
|
531
531
|
if (!IP_BLOCKING_CONFIG.blacklist.automatic.enabled) {
|
|
532
532
|
return null;
|
|
533
533
|
}
|
|
@@ -535,7 +535,7 @@ async function checkAutoIPBlocking(ip) {
|
|
|
535
535
|
const config = IP_BLOCKING_CONFIG.blacklist.automatic;
|
|
536
536
|
|
|
537
537
|
// 1. Verificar falhas por hora
|
|
538
|
-
const failuresPerHour = await DB.prepare(`
|
|
538
|
+
const failuresPerHour = await env.DB.prepare(`
|
|
539
539
|
SELECT COUNT(*) as failures
|
|
540
540
|
FROM ip_violations
|
|
541
541
|
WHERE ip = ?
|
|
@@ -544,7 +544,7 @@ async function checkAutoIPBlocking(ip) {
|
|
|
544
544
|
|
|
545
545
|
if (failuresPerHour.failures >= config.threshold_failures_per_hour) {
|
|
546
546
|
// Bloquear IP automaticamente
|
|
547
|
-
await blockIPAutomatically(ip, 'EXCEEDED_FAILURES_PER_HOUR', failuresPerHour.failures);
|
|
547
|
+
await blockIPAutomatically(ip, 'EXCEEDED_FAILURES_PER_HOUR', failuresPerHour.failures, env);
|
|
548
548
|
return {
|
|
549
549
|
blocked: true,
|
|
550
550
|
reason: 'EXCEEDED_FAILURES_PER_HOUR',
|
|
@@ -554,7 +554,7 @@ async function checkAutoIPBlocking(ip) {
|
|
|
554
554
|
}
|
|
555
555
|
|
|
556
556
|
// 2. Verificar falhas por dia
|
|
557
|
-
const failuresPerDay = await DB.prepare(`
|
|
557
|
+
const failuresPerDay = await env.DB.prepare(`
|
|
558
558
|
SELECT COUNT(*) as failures
|
|
559
559
|
FROM ip_violations
|
|
560
560
|
WHERE ip = ?
|
|
@@ -562,7 +562,7 @@ async function checkAutoIPBlocking(ip) {
|
|
|
562
562
|
`).bind(ip).get();
|
|
563
563
|
|
|
564
564
|
if (failuresPerDay.failures >= config.threshold_failures_per_day) {
|
|
565
|
-
await blockIPAutomatically(ip, 'EXCEEDED_FAILURES_PER_DAY', failuresPerDay.failures);
|
|
565
|
+
await blockIPAutomatically(ip, 'EXCEEDED_FAILURES_PER_DAY', failuresPerDay.failures, env);
|
|
566
566
|
return {
|
|
567
567
|
blocked: true,
|
|
568
568
|
reason: 'EXCEEDED_FAILURES_PER_DAY',
|
|
@@ -572,7 +572,7 @@ async function checkAutoIPBlocking(ip) {
|
|
|
572
572
|
}
|
|
573
573
|
|
|
574
574
|
// 3. Verificar erros 429 por hora
|
|
575
|
-
const rateLimitErrorsPerHour = await DB.prepare(`
|
|
575
|
+
const rateLimitErrorsPerHour = await env.DB.prepare(`
|
|
576
576
|
SELECT COUNT(*) as errors
|
|
577
577
|
FROM ip_violations
|
|
578
578
|
WHERE ip = ?
|
|
@@ -581,7 +581,7 @@ async function checkAutoIPBlocking(ip) {
|
|
|
581
581
|
`).bind(ip).get();
|
|
582
582
|
|
|
583
583
|
if (rateLimitErrorsPerHour.errors >= config.threshold_429_per_hour) {
|
|
584
|
-
await blockIPAutomatically(ip, 'EXCEEDED_RATE_LIMITS_PER_HOUR', rateLimitErrorsPerHour.errors);
|
|
584
|
+
await blockIPAutomatically(ip, 'EXCEEDED_RATE_LIMITS_PER_HOUR', rateLimitErrorsPerHour.errors, env);
|
|
585
585
|
return {
|
|
586
586
|
blocked: true,
|
|
587
587
|
reason: 'EXCEEDED_RATE_LIMITS_PER_HOUR',
|
|
@@ -594,10 +594,10 @@ async function checkAutoIPBlocking(ip) {
|
|
|
594
594
|
}
|
|
595
595
|
|
|
596
596
|
// Bloquear IP automaticamente
|
|
597
|
-
async function blockIPAutomatically(ip, reason, count) {
|
|
597
|
+
async function blockIPAutomatically(ip, reason, count, env) {
|
|
598
598
|
const now = new Date().toISOString();
|
|
599
599
|
|
|
600
|
-
await DB.prepare(`
|
|
600
|
+
await env.DB.prepare(`
|
|
601
601
|
INSERT OR REPLACE INTO ip_blacklist
|
|
602
602
|
(ip, block_reason, blocked_at, blocking_type, violation_count, last_violation_type, last_violation_at)
|
|
603
603
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
@@ -925,7 +925,106 @@ export function sanitizePayload(eventData, eventName) {
|
|
|
925
925
|
}
|
|
926
926
|
```
|
|
927
927
|
|
|
928
|
-
### 3.3
|
|
928
|
+
### 3.3 CSRF Protection (Anti-Cross-Site Request Forgery)
|
|
929
|
+
|
|
930
|
+
CSRF é relevante nos **endpoints de webhook** (Hotmart, Kiwify, Ticto) onde um atacante pode forjar requisições. A proteção é HMAC-SHA256 por assinatura — cada plataforma assina o payload com um secret compartilhado.
|
|
931
|
+
|
|
932
|
+
```javascript
|
|
933
|
+
/**
|
|
934
|
+
* Verificação CSRF via HMAC-SHA256 para webhooks de plataformas de pagamento.
|
|
935
|
+
* Cada plataforma tem seu próprio header e algoritmo.
|
|
936
|
+
*
|
|
937
|
+
* @param {Request} request
|
|
938
|
+
* @param {Object} env
|
|
939
|
+
* @param {string} gateway - 'hotmart' | 'kiwify' | 'ticto' | 'stripe'
|
|
940
|
+
* @returns {Promise<boolean>} true se assinatura válida
|
|
941
|
+
*/
|
|
942
|
+
export async function validateWebhookSignature(request, env, gateway) {
|
|
943
|
+
const body = await request.text(); // Ler como texto para HMAC exato
|
|
944
|
+
|
|
945
|
+
switch (gateway) {
|
|
946
|
+
case 'hotmart': {
|
|
947
|
+
// Hotmart: header X-Hotmart-Hottok (token fixo, não HMAC)
|
|
948
|
+
const token = request.headers.get('X-Hotmart-Hottok');
|
|
949
|
+
return token === env.WEBHOOK_SECRET_HOTMART;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
case 'kiwify': {
|
|
953
|
+
// Kiwify: query param ?signature=HMAC_SHA256(body, secret)
|
|
954
|
+
const url = new URL(request.url);
|
|
955
|
+
const receivedSig = url.searchParams.get('signature') || '';
|
|
956
|
+
const expectedSig = await hmacSHA256(body, env.WEBHOOK_SECRET_KIWIFY);
|
|
957
|
+
return timingSafeEqual(receivedSig, expectedSig);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
case 'ticto': {
|
|
961
|
+
// Ticto: header X-Ticto-Signature = HMAC_SHA256(body, secret)
|
|
962
|
+
const receivedSig = request.headers.get('X-Ticto-Signature') || '';
|
|
963
|
+
const expectedSig = await hmacSHA256(body, env.WEBHOOK_SECRET_TICTO);
|
|
964
|
+
return timingSafeEqual(receivedSig, expectedSig);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
case 'stripe': {
|
|
968
|
+
// Stripe: header Stripe-Signature = t={ts},v1={HMAC}
|
|
969
|
+
const sigHeader = request.headers.get('Stripe-Signature') || '';
|
|
970
|
+
const parts = Object.fromEntries(sigHeader.split(',').map(p => p.split('=')));
|
|
971
|
+
const signedPayload = `${parts.t}.${body}`;
|
|
972
|
+
const expectedSig = await hmacSHA256(signedPayload, env.STRIPE_WEBHOOK_SECRET);
|
|
973
|
+
return timingSafeEqual(parts.v1, expectedSig);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
default:
|
|
977
|
+
return false; // Gateway desconhecido = rejeitar
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// HMAC-SHA256 usando WebCrypto (disponível em Cloudflare Workers)
|
|
982
|
+
async function hmacSHA256(message, secret) {
|
|
983
|
+
const encoder = new TextEncoder();
|
|
984
|
+
const key = await crypto.subtle.importKey(
|
|
985
|
+
'raw', encoder.encode(secret),
|
|
986
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
987
|
+
false, ['sign']
|
|
988
|
+
);
|
|
989
|
+
const sig = await crypto.subtle.sign('HMAC', key, encoder.encode(message));
|
|
990
|
+
return Array.from(new Uint8Array(sig)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Comparação em tempo constante — previne timing attacks
|
|
994
|
+
function timingSafeEqual(a, b) {
|
|
995
|
+
if (a.length !== b.length) return false;
|
|
996
|
+
let diff = 0;
|
|
997
|
+
for (let i = 0; i < a.length; i++) {
|
|
998
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
999
|
+
}
|
|
1000
|
+
return diff === 0;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* Uso no handler de webhook:
|
|
1005
|
+
*
|
|
1006
|
+
* const isValid = await validateWebhookSignature(request, env, 'hotmart');
|
|
1007
|
+
* if (!isValid) return new Response('Unauthorized', { status: 401 });
|
|
1008
|
+
*
|
|
1009
|
+
* REGRA: Validar assinatura ANTES de parsear o body JSON.
|
|
1010
|
+
* Re-clonar o request se precisar ler o body depois:
|
|
1011
|
+
* const clonedRequest = request.clone();
|
|
1012
|
+
* const valid = await validateWebhookSignature(clonedRequest, env, gateway);
|
|
1013
|
+
* const body = await request.json(); // original ainda disponível
|
|
1014
|
+
*/
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
### Checklist CSRF
|
|
1018
|
+
|
|
1019
|
+
- [ ] HMAC validado para Hotmart, Kiwify, Ticto antes de processar
|
|
1020
|
+
- [ ] Rejeição 401 imediata se assinatura inválida
|
|
1021
|
+
- [ ] Uso de `timingSafeEqual` para prevenir timing attacks
|
|
1022
|
+
- [ ] Body lido como texto para HMAC (não como JSON — evita parsing antes da validação)
|
|
1023
|
+
- [ ] Secrets via `wrangler secret put WEBHOOK_SECRET_HOTMART` etc.
|
|
1024
|
+
|
|
1025
|
+
---
|
|
1026
|
+
|
|
1027
|
+
### 3.4 Middleware de Validação e Sanitização
|
|
929
1028
|
|
|
930
1029
|
```javascript
|
|
931
1030
|
// Middleware de segurança completo
|
|
@@ -1401,7 +1500,7 @@ const SEVERITY_LEVELS = {
|
|
|
1401
1500
|
|
|
1402
1501
|
```javascript
|
|
1403
1502
|
// Log de evento de segurança
|
|
1404
|
-
export async function logSecurityEvent(eventData) {
|
|
1503
|
+
export async function logSecurityEvent(eventData, env) {
|
|
1405
1504
|
const {
|
|
1406
1505
|
type,
|
|
1407
1506
|
severity,
|
|
@@ -1419,7 +1518,7 @@ export async function logSecurityEvent(eventData) {
|
|
|
1419
1518
|
|
|
1420
1519
|
const timestamp = new Date().toISOString();
|
|
1421
1520
|
|
|
1422
|
-
await DB.prepare(`
|
|
1521
|
+
await env.DB.prepare(`
|
|
1423
1522
|
INSERT INTO audit_logs
|
|
1424
1523
|
(timestamp, ip, user_id, session_id, user_agent, event_name, event_id,
|
|
1425
1524
|
log_type, severity, action, outcome, details, blocked)
|
|
@@ -1453,7 +1552,7 @@ export async function logSecurityEvent(eventData) {
|
|
|
1453
1552
|
}
|
|
1454
1553
|
|
|
1455
1554
|
// Query de audit logs
|
|
1456
|
-
export async function queryAuditLogs(filters = {}) {
|
|
1555
|
+
export async function queryAuditLogs(filters = {}, env) {
|
|
1457
1556
|
const {
|
|
1458
1557
|
ip,
|
|
1459
1558
|
user_id,
|
|
@@ -1509,7 +1608,7 @@ export async function queryAuditLogs(filters = {}) {
|
|
|
1509
1608
|
|
|
1510
1609
|
query += ' ORDER BY timestamp DESC LIMIT ?';
|
|
1511
1610
|
|
|
1512
|
-
const results = await DB.prepare(query).bind(...params).all();
|
|
1611
|
+
const results = await env.DB.prepare(query).bind(...params).all();
|
|
1513
1612
|
|
|
1514
1613
|
return results;
|
|
1515
1614
|
}
|
|
@@ -1539,7 +1638,7 @@ export async function getRateLimitStatus(request, env) {
|
|
|
1539
1638
|
refill_rate: rateLimiters.event.get('global').refillRate
|
|
1540
1639
|
}
|
|
1541
1640
|
},
|
|
1542
|
-
recent_violations: await DB.prepare(`
|
|
1641
|
+
recent_violations: await env.DB.prepare(`
|
|
1543
1642
|
SELECT
|
|
1544
1643
|
log_type,
|
|
1545
1644
|
severity,
|
|
@@ -1566,9 +1665,9 @@ export async function getIPStatus(request, env) {
|
|
|
1566
1665
|
const ip = request.headers.get('CF-Connecting-IP') || 'unknown';
|
|
1567
1666
|
|
|
1568
1667
|
// Verificar status do IP
|
|
1569
|
-
const blacklist = await checkIPBlacklist(ip);
|
|
1570
|
-
const whitelist = await checkIPWhitelist(ip);
|
|
1571
|
-
const geoBlock = await checkGeoBlocking(request);
|
|
1668
|
+
const blacklist = await checkIPBlacklist(ip, env);
|
|
1669
|
+
const whitelist = await checkIPWhitelist(ip, env);
|
|
1670
|
+
const geoBlock = await checkGeoBlocking(request, env);
|
|
1572
1671
|
|
|
1573
1672
|
const status = {
|
|
1574
1673
|
ip,
|
|
@@ -1577,7 +1676,7 @@ export async function getIPStatus(request, env) {
|
|
|
1577
1676
|
blacklist_reason: blacklist ? blacklist.block_reason : null,
|
|
1578
1677
|
is_geo_blocked: !!geoBlock,
|
|
1579
1678
|
geo_details: geoBlock || null,
|
|
1580
|
-
recent_violations: await DB.prepare(`
|
|
1679
|
+
recent_violations: await env.DB.prepare(`
|
|
1581
1680
|
SELECT
|
|
1582
1681
|
COUNT(*) as violations,
|
|
1583
1682
|
MAX(violation_count) as max_violation_count
|
|
@@ -1699,6 +1798,16 @@ export const SEVERITY_LEVELS = { ... };
|
|
|
1699
1798
|
- [ ] CIDR ranges implementados
|
|
1700
1799
|
- [ ] Auto-unblock implementado
|
|
1701
1800
|
|
|
1801
|
+
### CSRF Protection (Webhooks)
|
|
1802
|
+
|
|
1803
|
+
- [ ] HMAC-SHA256 validado para Hotmart
|
|
1804
|
+
- [ ] HMAC-SHA256 validado para Kiwify
|
|
1805
|
+
- [ ] HMAC-SHA256 validado para Ticto
|
|
1806
|
+
- [ ] HMAC-SHA256 validado para Stripe
|
|
1807
|
+
- [ ] `timingSafeEqual` implementado (sem timing attacks)
|
|
1808
|
+
- [ ] Body lido como text antes do JSON.parse para validação HMAC
|
|
1809
|
+
- [ ] Secrets via `wrangler secret put` (nunca hardcode)
|
|
1810
|
+
|
|
1702
1811
|
### Input Validation
|
|
1703
1812
|
|
|
1704
1813
|
- [ ] Joi schemas criados (Lead, Purchase, Contact)
|
|
@@ -89,11 +89,10 @@ UMBRELLA_DOMAIN = "dominio.com"
|
|
|
89
89
|
# META_ACCESS_TOKEN ← obrigatório
|
|
90
90
|
# GA4_API_SECRET ← obrigatório
|
|
91
91
|
# TIKTOK_ACCESS_TOKEN ← opcional
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
# WA_WEBHOOK_VERIFY_TOKEN ← gerado pelo agente (crypto.randomUUID)
|
|
92
|
+
# WHATSAPP_ACCESS_TOKEN ← WhatsApp Cloud API — token de acesso permanente
|
|
93
|
+
# WHATSAPP_PHONE_NUMBER_ID ← WhatsApp Cloud API — Phone Number ID (ex: 123456789012345)
|
|
94
|
+
# WA_NOTIFY_NUMBER ← Número do dono para receber notificações (ex: 5511999998888)
|
|
95
|
+
# WA_WEBHOOK_VERIFY_TOKEN ← Token de verificação do webhook CTWA (gerado via crypto.randomUUID)
|
|
97
96
|
# PINTEREST_ACCESS_TOKEN ← ativar Pinterest CAPI v5
|
|
98
97
|
# PINTEREST_AD_ACCOUNT_ID ← ativar Pinterest CAPI v5
|
|
99
98
|
# REDDIT_ACCESS_TOKEN ← ativar Reddit CAPI v2.0
|
|
@@ -1050,7 +1049,7 @@ Timestamp: ${new Date().toISOString()}
|
|
|
1050
1049
|
`.trim();
|
|
1051
1050
|
|
|
1052
1051
|
// Verificar se há token do WhatsApp configurado
|
|
1053
|
-
const waPhoneId = env.
|
|
1052
|
+
const waPhoneId = env.WHATSAPP_PHONE_NUMBER_ID;
|
|
1054
1053
|
const adminNumber = env.ADMIN_PHONE_NUMBER;
|
|
1055
1054
|
|
|
1056
1055
|
if (waPhoneId && adminNumber) {
|
|
@@ -1058,7 +1057,7 @@ Timestamp: ${new Date().toISOString()}
|
|
|
1058
1057
|
method: 'POST',
|
|
1059
1058
|
headers: {
|
|
1060
1059
|
'Content-Type': 'application/json',
|
|
1061
|
-
'Authorization': `Bearer ${env.
|
|
1060
|
+
'Authorization': `Bearer ${env.WHATSAPP_ACCESS_TOKEN}`
|
|
1062
1061
|
},
|
|
1063
1062
|
body: JSON.stringify({
|
|
1064
1063
|
messaging_product: 'whatsapp',
|
|
@@ -1145,7 +1144,7 @@ export async function queue(batch, env) {
|
|
|
1145
1144
|
- Plataformas selecionadas na FASE 0-B (Meta, Google, TikTok, etc.)
|
|
1146
1145
|
- `UMBRELLA_DOMAIN` — domínio principal do funil (detectado automaticamente ou fornecido pelo usuário)
|
|
1147
1146
|
- Secrets de plataformas: `META_ACCESS_TOKEN`, `GA4_API_SECRET`, `TIKTOK_ACCESS_TOKEN`
|
|
1148
|
-
- Secrets opcionais: `RESEND_API_KEY`, `
|
|
1147
|
+
- Secrets opcionais: `RESEND_API_KEY`, `WHATSAPP_ACCESS_TOKEN`, `WHATSAPP_PHONE_NUMBER_ID`
|
|
1149
1148
|
|
|
1150
1149
|
## RESPONSABILIDADE
|
|
1151
1150
|
|
|
@@ -73,6 +73,69 @@ Gere payloads para o Worker seguir a API oficial:
|
|
|
73
73
|
|
|
74
74
|
---
|
|
75
75
|
|
|
76
|
+
## ⏱️ RATE LIMITS — TikTok Events API v1.3
|
|
77
|
+
|
|
78
|
+
Conforme `contracts/api-versions.json`, a TikTok Events API tem limites estritos:
|
|
79
|
+
|
|
80
|
+
| Limite | Valor | Ação se excedido |
|
|
81
|
+
|--------|-------|-----------------|
|
|
82
|
+
| Requisições por minuto (por pixel) | 10 req/min | Implementar throttling |
|
|
83
|
+
| Eventos por batch | 5 events/batch | Agrupar eventos em batches |
|
|
84
|
+
| Retries máximos | 3 tentativas | Backoff exponencial |
|
|
85
|
+
|
|
86
|
+
### Implementação de Throttling no Worker
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
// Rate limit KV key: 'tiktok_rate_{pixel_id}_{minute}'
|
|
90
|
+
async function dispatchTikTokWithRateLimit(env, events, pixelId, accessToken) {
|
|
91
|
+
const now = new Date();
|
|
92
|
+
const minuteKey = `tiktok_rate_${pixelId}_${now.getUTCFullYear()}${now.getUTCMonth()}${now.getUTCDate()}${now.getUTCHours()}${now.getUTCMinutes()}`;
|
|
93
|
+
|
|
94
|
+
// Verificar rate limit no KV
|
|
95
|
+
const currentCount = parseInt(await env.GEO_CACHE.get(minuteKey) || '0');
|
|
96
|
+
|
|
97
|
+
if (currentCount >= 10) {
|
|
98
|
+
// Rate limit atingido — encaminhar para RETRY_QUEUE
|
|
99
|
+
await env.RETRY_QUEUE.send({ platform: 'tiktok', events, pixelId });
|
|
100
|
+
return { queued: true, reason: 'rate_limit' };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Agrupar eventos em batches de 5
|
|
104
|
+
const batches = [];
|
|
105
|
+
for (let i = 0; i < events.length; i += 5) {
|
|
106
|
+
batches.push(events.slice(i, i + 5));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const results = [];
|
|
110
|
+
for (const batch of batches) {
|
|
111
|
+
const result = await fetch('https://business-api.tiktok.com/open_api/v1.3/event/track/', {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: {
|
|
114
|
+
'Content-Type': 'application/json',
|
|
115
|
+
'Access-Token': accessToken
|
|
116
|
+
},
|
|
117
|
+
body: JSON.stringify({
|
|
118
|
+
pixel_code: pixelId,
|
|
119
|
+
event_source: 'web',
|
|
120
|
+
event_source_id: pixelId,
|
|
121
|
+
data: batch
|
|
122
|
+
})
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Incrementar contador no KV (TTL de 60s = 1 minuto)
|
|
126
|
+
await env.GEO_CACHE.put(minuteKey, String(currentCount + 1), { expirationTtl: 60 });
|
|
127
|
+
|
|
128
|
+
results.push(result);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { sent: results.length, batches: batches.length };
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
> **Regra:** Se `HTTP 429` for recebido da TikTok API, encaminhar eventos para `RETRY_QUEUE` com backoff de 1min, 2min, 4min (máximo 3 tentativas).
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
76
139
|
## INPUTS RECEBIDOS
|
|
77
140
|
|
|
78
141
|
- JSON do Page Analyzer Agent (eventos mapeados, seletores, tipo de página)
|
|
@@ -59,19 +59,30 @@ function validateEventCoverage(pageAnalysis, agentOutputs) {
|
|
|
59
59
|
});
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
// Encontrar eventos
|
|
63
|
-
const
|
|
62
|
+
// Encontrar eventos do Page Analyzer NÃO implementados por nenhum agente
|
|
63
|
+
const unimplementedEvents = [];
|
|
64
|
+
pageEvents.forEach(eventKey => {
|
|
65
|
+
if (!agentEvents.has(eventKey)) {
|
|
66
|
+
unimplementedEvents.push(eventKey);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Encontrar eventos implementados pelos agentes mas SEM correspondência no Page Analyzer
|
|
71
|
+
const orphanEvents = [];
|
|
64
72
|
agentEvents.forEach(eventKey => {
|
|
65
73
|
if (!pageEvents.has(eventKey)) {
|
|
66
|
-
|
|
74
|
+
orphanEvents.push(eventKey);
|
|
67
75
|
}
|
|
68
76
|
});
|
|
69
77
|
|
|
70
78
|
return {
|
|
71
79
|
total_page_events: pageEvents.size,
|
|
72
80
|
total_implemented_events: agentEvents.size,
|
|
73
|
-
|
|
74
|
-
|
|
81
|
+
unimplemented_events: unimplementedEvents, // ← Eventos do plano sem código
|
|
82
|
+
orphan_events: orphanEvents, // ← Código sem evento no plano
|
|
83
|
+
coverage_percentage: Math.round(
|
|
84
|
+
((pageEvents.size - unimplementedEvents.length) / Math.max(pageEvents.size, 1)) * 100
|
|
85
|
+
)
|
|
75
86
|
};
|
|
76
87
|
}
|
|
77
88
|
```
|
|
@@ -187,6 +198,90 @@ async function validateApiVersions(trackingPlan) {
|
|
|
187
198
|
}
|
|
188
199
|
```
|
|
189
200
|
|
|
201
|
+
### 1.5 Validação Cruzada Completa (runFullValidation)
|
|
202
|
+
|
|
203
|
+
```javascript
|
|
204
|
+
/**
|
|
205
|
+
* Ponto de entrada principal — executa TODAS as validações em sequência
|
|
206
|
+
* e retorna um relatório consolidado com status PASS | WARN | BLOCK
|
|
207
|
+
*
|
|
208
|
+
* @param {Object} pageAnalysis - Output do Page Analyzer Agent
|
|
209
|
+
* @param {Object} agentOutputs - Código gerado por todos os agentes
|
|
210
|
+
* @param {Object} apiVersions - Conteúdo de contracts/api-versions.json
|
|
211
|
+
* @returns {Object} Relatório consolidado de validação
|
|
212
|
+
*/
|
|
213
|
+
async function runFullValidation(pageAnalysis, agentOutputs, apiVersions) {
|
|
214
|
+
const report = {
|
|
215
|
+
status: 'PASS', // PASS | WARN | BLOCK
|
|
216
|
+
timestamp: new Date().toISOString(),
|
|
217
|
+
checks: {}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// CHECK 1: Cobertura de eventos
|
|
221
|
+
const coverage = validateEventCoverage(pageAnalysis, agentOutputs);
|
|
222
|
+
report.checks.event_coverage = coverage;
|
|
223
|
+
if (coverage.coverage_percentage < 100) {
|
|
224
|
+
report.status = coverage.coverage_percentage < 80 ? 'BLOCK' : 'WARN';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// CHECK 2: Parâmetros de conversão
|
|
228
|
+
const allEvents = Object.values(agentOutputs).flatMap(o => o.events || []);
|
|
229
|
+
const paramIssues = validateConversionParameters(allEvents, apiVersions);
|
|
230
|
+
report.checks.conversion_params = paramIssues;
|
|
231
|
+
if (paramIssues.filter(i => i.severity === 'HIGH').length > 0) {
|
|
232
|
+
report.status = 'BLOCK';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// CHECK 3: Seletores existentes no código
|
|
236
|
+
const missingSelectors = validateSelectorsExist({}, pageAnalysis);
|
|
237
|
+
report.checks.selectors = { missing: missingSelectors };
|
|
238
|
+
if (missingSelectors.length > 0) {
|
|
239
|
+
if (report.status === 'PASS') report.status = 'WARN';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// CHECK 4: Versões de API consistentes com api-versions.json
|
|
243
|
+
const trackingPlan = { events: {} };
|
|
244
|
+
Object.entries(agentOutputs).forEach(([agent, output]) => {
|
|
245
|
+
if (output.events) trackingPlan.events[agent] = output.events;
|
|
246
|
+
});
|
|
247
|
+
await validateApiVersions(trackingPlan);
|
|
248
|
+
const apiIssues = Object.values(trackingPlan.events)
|
|
249
|
+
.flatMap(events => events.filter(e => e.api_version_issue || e.api_deprecated));
|
|
250
|
+
report.checks.api_versions = apiIssues;
|
|
251
|
+
if (apiIssues.some(e => e.api_deprecated)) {
|
|
252
|
+
report.status = 'BLOCK'; // API depreciada = bloquear deploy
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// CHECK 5: Regras de ouro — deduplicação event_id
|
|
256
|
+
const missingEventId = allEvents.filter(e => !e.event_id && !e.params?.event_id);
|
|
257
|
+
report.checks.deduplication = {
|
|
258
|
+
events_missing_event_id: missingEventId.map(e => `${e.platform}:${e.name}`)
|
|
259
|
+
};
|
|
260
|
+
if (missingEventId.length > 0) report.status = 'WARN';
|
|
261
|
+
|
|
262
|
+
// CHECK 6: SHA-256 em campos PII
|
|
263
|
+
const piiFields = ['em', 'ph', 'fn', 'ln'];
|
|
264
|
+
const unhashed = allEvents.filter(e =>
|
|
265
|
+
piiFields.some(field => e.user_data?.[field] && !e.user_data[field].match(/^[a-f0-9]{64}$/))
|
|
266
|
+
);
|
|
267
|
+
report.checks.pii_hashing = {
|
|
268
|
+
events_with_unhashed_pii: unhashed.map(e => `${e.platform}:${e.name}`)
|
|
269
|
+
};
|
|
270
|
+
if (unhashed.length > 0) {
|
|
271
|
+
report.status = 'BLOCK'; // PII sem hash = bloquear imediatamente
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Determinar mensagem de status
|
|
275
|
+
report.summary = {
|
|
276
|
+
PASS: '✅ Tracking Plan validado — pode fazer deploy',
|
|
277
|
+
WARN: '⚠️ Tracking Plan com alertas — revisar antes do deploy',
|
|
278
|
+
BLOCK: '❌ BLOQUEADO — corrigir itens críticos antes do deploy'
|
|
279
|
+
}[report.status];
|
|
280
|
+
|
|
281
|
+
return report;
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
190
285
|
---
|
|
191
286
|
|
|
192
287
|
## PASSO 2 — GERAR O TRACKING PLAN COM VALIDAÇÃO
|
|
@@ -12,6 +12,48 @@ Você é o especialista em Webhooks do CDP Edge. Sua missão é capturar vendas
|
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
+
## 🔐 NORMALIZAÇÃO E HASHING DE PII (OBRIGATÓRIO)
|
|
16
|
+
|
|
17
|
+
Antes de qualquer dispatch para CAPI, normalizar e hashear PII extraída do webhook:
|
|
18
|
+
|
|
19
|
+
```javascript
|
|
20
|
+
// Hashing SHA-256 para PII — usar WebCrypto (disponível em Cloudflare Workers)
|
|
21
|
+
async function hashPII(value) {
|
|
22
|
+
if (!value) return null;
|
|
23
|
+
const normalized = value.toString().toLowerCase().trim();
|
|
24
|
+
const encoder = new TextEncoder();
|
|
25
|
+
const data = encoder.encode(normalized);
|
|
26
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
27
|
+
return Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Normalização E.164 para telefone (Brasil)
|
|
31
|
+
function normalizePhone(phone) {
|
|
32
|
+
if (!phone) return null;
|
|
33
|
+
const digits = phone.replace(/\D/g, '');
|
|
34
|
+
// Adicionar +55 se não tiver código de país
|
|
35
|
+
if (digits.length === 10 || digits.length === 11) return `+55${digits}`;
|
|
36
|
+
if (digits.startsWith('55') && (digits.length === 12 || digits.length === 13)) return `+${digits}`;
|
|
37
|
+
return `+${digits}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Exemplo de uso no handler de webhook:
|
|
41
|
+
async function hashWebhookUserData(webhookPayload) {
|
|
42
|
+
const email = webhookPayload.buyer?.email || webhookPayload.email;
|
|
43
|
+
const phone = webhookPayload.buyer?.phone || webhookPayload.phone;
|
|
44
|
+
return {
|
|
45
|
+
em: email ? await hashPII(email) : null, // SHA-256 lowercase+trim
|
|
46
|
+
ph: phone ? await hashPII(normalizePhone(phone)) : null, // SHA-256 após E.164
|
|
47
|
+
fn: webhookPayload.buyer?.first_name ? await hashPII(webhookPayload.buyer.first_name) : null,
|
|
48
|
+
ln: webhookPayload.buyer?.last_name ? await hashPII(webhookPayload.buyer.last_name) : null,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
> **Regra:** NUNCA enviar email ou telefone em plaintext para Meta CAPI, GA4 MP ou TikTok Events API. Sempre normalizar → hashear → enviar.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
15
57
|
## 🏗️ PADRÕES TÉCNICOS (Quantum Tier)
|
|
16
58
|
|
|
17
59
|
1. **D1 Identity Cross-Check**: Utilize o e-mail ou telefone do webhook para buscar no banco **D1** os identificadores originais (`fbp`, `fbc`, `ttp`). Isso garante a precisão da atribuição.
|
|
@@ -29,6 +71,64 @@ Você é o especialista em Webhooks do CDP Edge. Sua missão é capturar vendas
|
|
|
29
71
|
|
|
30
72
|
---
|
|
31
73
|
|
|
74
|
+
## 🔗 INTEGRAÇÃO COM OUTROS AGENTES (Fluxo Pós-Compra)
|
|
75
|
+
|
|
76
|
+
Após processar um webhook de compra com sucesso, o Webhook Agent DEVE disparar:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
Webhook (Hotmart/Kiwify/Ticto/Stripe)
|
|
80
|
+
│
|
|
81
|
+
├─► [1] Validar HMAC → rejeitar 401 se inválido
|
|
82
|
+
├─► [2] Dedup D1 por transaction_id
|
|
83
|
+
├─► [3] Cross-check D1 por email → fbp/fbc/ttp/gclid
|
|
84
|
+
├─► [4] Hashear PII (SHA-256) — ver seção acima
|
|
85
|
+
│
|
|
86
|
+
├─► [5] CAPI Dispatch (ctx.waitUntil) — paralelo:
|
|
87
|
+
│ → Meta CAPI v22.0 (Purchase)
|
|
88
|
+
│ → GA4 MP (purchase)
|
|
89
|
+
│ → TikTok Events API v1.3 (CompletePayment)
|
|
90
|
+
│
|
|
91
|
+
├─► [6] Email Agent (ctx.waitUntil) — enviar confirmação de compra:
|
|
92
|
+
│ → Chamar sendEmail(env, 'purchase_confirmation', { email, nome, produto, valor })
|
|
93
|
+
│ → Ver email-agent.md para implementação completa
|
|
94
|
+
│
|
|
95
|
+
└─► [7] CRM Sync (ctx.waitUntil) — sincronizar comprador:
|
|
96
|
+
→ Chamar syncToCRM(env, 'purchase', { email, nome, produto, valor, order_id })
|
|
97
|
+
→ Ver crm-integration-agent.md para implementação completa
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Código de Integração (webhook handler)
|
|
101
|
+
|
|
102
|
+
```javascript
|
|
103
|
+
// No handler de webhook, após validação e dedup:
|
|
104
|
+
ctx.waitUntil(Promise.allSettled([
|
|
105
|
+
// [5] CAPI dispatch
|
|
106
|
+
dispatchToCAPI(env, hashedUserData, purchaseData),
|
|
107
|
+
|
|
108
|
+
// [6] Email Agent — confirmação de compra
|
|
109
|
+
sendEmail(env, 'purchase_confirmation', {
|
|
110
|
+
to: buyerEmail,
|
|
111
|
+
name: buyerName,
|
|
112
|
+
product: productName,
|
|
113
|
+
value: purchaseValue
|
|
114
|
+
}),
|
|
115
|
+
|
|
116
|
+
// [7] CRM Sync — criar/atualizar contato como comprador
|
|
117
|
+
syncToCRM(env, 'purchase', {
|
|
118
|
+
email: buyerEmail,
|
|
119
|
+
name: buyerName,
|
|
120
|
+
product: productName,
|
|
121
|
+
value: purchaseValue,
|
|
122
|
+
order_id: transactionId
|
|
123
|
+
})
|
|
124
|
+
]));
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
> **Nota:** `sendEmail()` é implementada pelo Email Agent em `cloudflare/email-service.js`.
|
|
128
|
+
> `syncToCRM()` é implementada pelo CRM Integration Agent em `cloudflare/crm-service.js`.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
32
132
|
## INPUTS RECEBIDOS
|
|
33
133
|
|
|
34
134
|
- Payload JSON do webhook da plataforma de vendas (Hotmart, Kiwify, Ticto, Stripe)
|