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.
Files changed (34) hide show
  1. package/contracts/api-versions.json +12 -8
  2. package/dist/commands/install.js +1 -2
  3. package/dist/commands/setup.js +1 -2
  4. package/extracted-skill/tracking-events-generator/agents/attribution-agent.md +23 -23
  5. package/extracted-skill/tracking-events-generator/agents/browser-tracking.md +172 -72
  6. package/extracted-skill/tracking-events-generator/agents/compliance-agent.md +20 -0
  7. package/extracted-skill/tracking-events-generator/agents/crm-integration-agent.md +48 -16
  8. package/extracted-skill/tracking-events-generator/agents/dashboard-agent.md +7 -7
  9. package/extracted-skill/tracking-events-generator/agents/database-agent.md +8 -8
  10. package/extracted-skill/tracking-events-generator/agents/debug-agent.md +13 -13
  11. package/extracted-skill/tracking-events-generator/agents/devops-agent.md +31 -7
  12. package/extracted-skill/tracking-events-generator/agents/email-agent.md +27 -0
  13. package/extracted-skill/tracking-events-generator/agents/fingerprint-agent.md +205 -0
  14. package/extracted-skill/tracking-events-generator/agents/google-agent.md +118 -0
  15. package/extracted-skill/tracking-events-generator/agents/intelligence-agent.md +90 -4
  16. package/extracted-skill/tracking-events-generator/agents/intelligence-scheduling.md +8 -641
  17. package/extracted-skill/tracking-events-generator/agents/linkedin-agent.md +108 -0
  18. package/extracted-skill/tracking-events-generator/agents/ltv-predictor-agent.md +1 -1
  19. package/extracted-skill/tracking-events-generator/agents/master-feedback-loop.md +68 -8
  20. package/extracted-skill/tracking-events-generator/agents/master-orchestrator.md +61 -18
  21. package/extracted-skill/tracking-events-generator/agents/memory-agent.md +98 -0
  22. package/extracted-skill/tracking-events-generator/agents/performance-agent.md +29 -19
  23. package/extracted-skill/tracking-events-generator/agents/performance-optimization-agent.md +11 -1
  24. package/extracted-skill/tracking-events-generator/agents/security-enterprise-agent.md +137 -28
  25. package/extracted-skill/tracking-events-generator/agents/server-tracking.md +7 -8
  26. package/extracted-skill/tracking-events-generator/agents/tiktok-agent.md +63 -0
  27. package/extracted-skill/tracking-events-generator/agents/tracking-plan-agent.md +100 -5
  28. package/extracted-skill/tracking-events-generator/agents/webhook-agent.md +100 -0
  29. package/extracted-skill/tracking-events-generator/agents/whatsapp-agent.md +58 -5
  30. package/extracted-skill/tracking-events-generator/agents/whatsapp-ctwa-setup-agent.md +16 -16
  31. package/extracted-skill/tracking-events-generator/agents/youtube-agent.md +140 -25
  32. package/extracted-skill/tracking-events-generator/contracts/api-versions.json +12 -8
  33. package/package.json +2 -2
  34. 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 Middleware de Validação e Sanitização
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
- # WA_ACCESS_TOKEN ← WhatsApp notificações ao dono
93
- # WA_PHONE_ID ← WhatsApp notificações ao dono
94
- # WHATSAPP_TOKEN WhatsApp Cloud API (CTWA webhook)
95
- # WHATSAPP_PHONE_NUMBER_ID WhatsApp Cloud API (CTWA webhook)
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.WA_PHONE_ID;
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.WA_ACCESS_TOKEN}`
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`, `WA_ACCESS_TOKEN`, `WA_PHONE_ID`
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 faltantes (no plano mas implementados)
63
- const missingEvents = [];
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
- missingEvents.push(eventKey);
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
- missing_events,
74
- coverage_percentage: Math.round((agentEvents.size / pageEvents.size) * 100)
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)