cdp-edge 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +153 -306
  2. package/bin/cdp-edge.js +71 -61
  3. package/contracts/agent-versions.json +682 -0
  4. package/contracts/api-versions.json +372 -368
  5. package/contracts/types.ts +81 -0
  6. package/dist/commands/analyze.js +52 -52
  7. package/dist/commands/infra.js +54 -54
  8. package/dist/commands/install.js +26 -3
  9. package/dist/commands/server.js +174 -174
  10. package/dist/commands/setup.js +332 -100
  11. package/dist/commands/validate.js +248 -84
  12. package/dist/index.js +12 -12
  13. package/dist/sdk/cdpTrack.js +2095 -0
  14. package/dist/sdk/cdpTrack.min.js +64 -0
  15. package/dist/sdk/install-snippet.html +10 -0
  16. package/docs/whatsapp-ctwa.md +5 -4
  17. package/extracted-skill/tracking-events-generator/INTEGRACAO-COMPLETA.md +89 -0
  18. package/extracted-skill/tracking-events-generator/MELHORIAS-IMPLEMENTADAS.md +101 -0
  19. package/extracted-skill/tracking-events-generator/advanced-matching.js +364 -364
  20. package/extracted-skill/tracking-events-generator/agents/ab-ltv-agent.md +196 -0
  21. package/extracted-skill/tracking-events-generator/agents/ab-testing-agent.md +1 -1
  22. package/extracted-skill/tracking-events-generator/agents/attribution-agent.md +41 -41
  23. package/extracted-skill/tracking-events-generator/agents/bidding-agent.md +347 -0
  24. package/extracted-skill/tracking-events-generator/agents/bing-agent.md +40 -50
  25. package/extracted-skill/tracking-events-generator/agents/browser-tracking.md +174 -74
  26. package/extracted-skill/tracking-events-generator/agents/code-guardian-agent.md +1 -1
  27. package/extracted-skill/tracking-events-generator/agents/compliance-agent.md +25 -5
  28. package/extracted-skill/tracking-events-generator/agents/dashboard-agent.md +10 -10
  29. package/extracted-skill/tracking-events-generator/agents/database-agent.md +43 -42
  30. package/extracted-skill/tracking-events-generator/agents/debug-agent.md +22 -22
  31. package/extracted-skill/tracking-events-generator/agents/devops-agent.md +232 -0
  32. package/extracted-skill/tracking-events-generator/agents/domain-setup-agent.md +23 -9
  33. package/extracted-skill/tracking-events-generator/agents/email-agent.md +28 -1
  34. package/extracted-skill/tracking-events-generator/agents/evo-crm-agent.md +244 -0
  35. package/extracted-skill/tracking-events-generator/agents/fingerprint-agent.md +206 -1
  36. package/extracted-skill/tracking-events-generator/agents/fraud-detection-agent.md +143 -0
  37. package/extracted-skill/tracking-events-generator/agents/google-agent.md +128 -2
  38. package/extracted-skill/tracking-events-generator/agents/intelligence-agent.md +191 -31
  39. package/extracted-skill/tracking-events-generator/agents/lead-scoring-agent.md +282 -0
  40. package/extracted-skill/tracking-events-generator/agents/linkedin-agent.md +145 -34
  41. package/extracted-skill/tracking-events-generator/agents/localization-agent.md +1 -1
  42. package/extracted-skill/tracking-events-generator/agents/ltv-predictor-agent.md +5 -5
  43. package/extracted-skill/tracking-events-generator/agents/master-feedback-loop.md +81 -21
  44. package/extracted-skill/tracking-events-generator/agents/master-orchestrator.md +428 -190
  45. package/extracted-skill/tracking-events-generator/agents/match-quality-agent.md +304 -0
  46. package/extracted-skill/tracking-events-generator/agents/memory-agent.json +25 -109
  47. package/extracted-skill/tracking-events-generator/agents/memory-agent.md +190 -15
  48. package/extracted-skill/tracking-events-generator/agents/meta-agent.md +10 -2
  49. package/extracted-skill/tracking-events-generator/agents/ml-clustering-agent.md +749 -0
  50. package/extracted-skill/tracking-events-generator/agents/page-analyzer.md +21 -4
  51. package/extracted-skill/tracking-events-generator/agents/performance-agent.md +41 -31
  52. package/extracted-skill/tracking-events-generator/agents/performance-optimization-agent.md +18 -8
  53. package/extracted-skill/tracking-events-generator/agents/pinterest-agent.md +14 -6
  54. package/extracted-skill/tracking-events-generator/agents/premium-tracking-intelligence-agent.md +7 -7
  55. package/extracted-skill/tracking-events-generator/agents/r2-setup-agent.md +16 -8
  56. package/extracted-skill/tracking-events-generator/agents/reddit-agent.md +15 -7
  57. package/extracted-skill/tracking-events-generator/agents/security-enterprise-agent.md +157 -48
  58. package/extracted-skill/tracking-events-generator/agents/server-tracking.md +35 -35
  59. package/extracted-skill/tracking-events-generator/agents/spotify-agent.md +15 -7
  60. package/extracted-skill/tracking-events-generator/agents/tiktok-agent.md +73 -2
  61. package/extracted-skill/tracking-events-generator/agents/tracking-plan-agent.md +104 -9
  62. package/extracted-skill/tracking-events-generator/agents/utm-agent.md +322 -0
  63. package/extracted-skill/tracking-events-generator/agents/validator-agent.md +13 -9
  64. package/extracted-skill/tracking-events-generator/agents/webhook-agent.md +112 -4
  65. package/extracted-skill/tracking-events-generator/agents/whatsapp-agent.md +58 -5
  66. package/extracted-skill/tracking-events-generator/agents/whatsapp-ctwa-setup-agent.md +26 -18
  67. package/extracted-skill/tracking-events-generator/agents/youtube-agent.md +152 -37
  68. package/extracted-skill/tracking-events-generator/anti-blocking.js +285 -285
  69. package/extracted-skill/tracking-events-generator/cdpTrack.js +642 -641
  70. package/extracted-skill/tracking-events-generator/contracts/api-versions.json +14 -10
  71. package/extracted-skill/tracking-events-generator/engagement-scoring.js +226 -226
  72. package/extracted-skill/tracking-events-generator/evals/evals.json +235 -235
  73. package/extracted-skill/tracking-events-generator/integration-test.js +497 -497
  74. package/extracted-skill/tracking-events-generator/knowledge-base.md +172 -0
  75. package/extracted-skill/tracking-events-generator/micro-events.js +992 -992
  76. package/extracted-skill/tracking-events-generator/models/lancamento-imobiliario.md +344 -0
  77. package/extracted-skill/tracking-events-generator/models/pinterest/conversions-api-template.js +144 -144
  78. package/extracted-skill/tracking-events-generator/models/pinterest/event-mappings.json +48 -48
  79. package/extracted-skill/tracking-events-generator/models/pinterest/tag-template.js +28 -28
  80. package/extracted-skill/tracking-events-generator/models/quiz-funnel.md +83 -19
  81. package/extracted-skill/tracking-events-generator/models/reddit/conversions-api-template.js +205 -205
  82. package/extracted-skill/tracking-events-generator/models/reddit/event-mappings.json +56 -56
  83. package/extracted-skill/tracking-events-generator/models/reddit/pixel-template.js +19 -19
  84. package/extracted-skill/tracking-events-generator/models/scenarios/behavior-engine.js +425 -425
  85. package/extracted-skill/tracking-events-generator/route-intent-capture.js +222 -0
  86. package/extracted-skill/tracking-events-generator/tracking.config.js +3 -3
  87. package/package.json +89 -75
  88. package/scripts/build-sdk.js +106 -0
  89. package/server-edge-tracker/.client.env.example +14 -0
  90. package/server-edge-tracker/INSTALAR.md +222 -23
  91. package/server-edge-tracker/SEGMENTATION-DOCS.md +513 -0
  92. package/server-edge-tracker/config/utm-mapping.json +64 -0
  93. package/server-edge-tracker/deploy-client.cjs +76 -0
  94. package/server-edge-tracker/index.ts +1230 -0
  95. package/server-edge-tracker/migrate-v7.sql +64 -0
  96. package/server-edge-tracker/modules/db.ts +710 -0
  97. package/server-edge-tracker/modules/dispatch/crm.ts +382 -0
  98. package/server-edge-tracker/modules/dispatch/ga4.ts +72 -0
  99. package/server-edge-tracker/modules/dispatch/meta.ts +143 -0
  100. package/server-edge-tracker/modules/dispatch/platforms.ts +255 -0
  101. package/server-edge-tracker/modules/dispatch/tiktok.ts +107 -0
  102. package/server-edge-tracker/modules/dispatch/whatsapp.ts +296 -0
  103. package/server-edge-tracker/modules/intelligence.ts +589 -0
  104. package/server-edge-tracker/modules/ml/bidding.ts +247 -0
  105. package/server-edge-tracker/modules/ml/fraud.ts +302 -0
  106. package/server-edge-tracker/modules/ml/logistic.ts +226 -0
  107. package/server-edge-tracker/modules/ml/ltv.ts +531 -0
  108. package/server-edge-tracker/modules/ml/matchquality.ts +232 -0
  109. package/server-edge-tracker/modules/ml/quiz.ts +343 -0
  110. package/server-edge-tracker/modules/ml/roas.ts +255 -0
  111. package/server-edge-tracker/modules/ml/segmentation.ts +407 -0
  112. package/server-edge-tracker/modules/nurture.ts +257 -0
  113. package/server-edge-tracker/modules/utils.ts +311 -0
  114. package/server-edge-tracker/modules/utm/utm-enricher.ts +231 -0
  115. package/server-edge-tracker/schema-ab-ltv.sql +97 -0
  116. package/server-edge-tracker/schema-bidding.sql +86 -0
  117. package/server-edge-tracker/schema-fraud.sql +90 -0
  118. package/server-edge-tracker/schema-indexes.sql +67 -0
  119. package/server-edge-tracker/schema-ltv-feedback.sql +11 -0
  120. package/server-edge-tracker/schema-quiz.sql +52 -0
  121. package/server-edge-tracker/schema-sales-engine.sql +113 -0
  122. package/server-edge-tracker/schema-segmentation.sql +219 -0
  123. package/server-edge-tracker/schema-utm.sql +82 -0
  124. package/server-edge-tracker/schema.sql +281 -265
  125. package/server-edge-tracker/types.ts +275 -0
  126. package/server-edge-tracker/wrangler.toml +140 -85
  127. package/templates/lancamento-imobiliario.md +344 -0
  128. package/templates/multi-step-checkout.md +3 -4
  129. package/templates/pinterest/conversions-api-template.js +144 -144
  130. package/templates/pinterest/event-mappings.json +48 -48
  131. package/templates/pinterest/tag-template.js +28 -28
  132. package/templates/quiz-funnel.md +83 -19
  133. package/templates/reddit/conversions-api-template.js +205 -205
  134. package/templates/reddit/event-mappings.json +56 -56
  135. package/templates/reddit/pixel-template.js +12 -39
  136. package/templates/scenarios/behavior-engine.js +45 -22
  137. package/docs/PixelBuilder-Documentacao-Completa (2).docx +0 -0
  138. package/docs/installation.md +0 -155
  139. package/docs/quick-start.md +0 -185
  140. package/extracted-skill/tracking-events-generator/agents/crm-integration-agent.md +0 -1419
  141. package/extracted-skill/tracking-events-generator/agents/intelligence-scheduling.md +0 -643
  142. package/server-edge-tracker/worker.js +0 -2574
@@ -0,0 +1,257 @@
1
+ /**
2
+ * CDP Edge — Nurture Engine (Fase 7)
3
+ *
4
+ * Sequências de follow-up automáticas baseadas na qualificação do quiz:
5
+ * comprador → contato imediato (já tratado pelo hot lead)
6
+ * interessado → D+1, D+3, D+7 (WhatsApp ou email)
7
+ * curioso → D+2, D+5 (conteúdo/isca)
8
+ * perdido → exclusão do remarketing (cohort_label = excluded)
9
+ *
10
+ * Arquitetura:
11
+ * 1. scheduleNurture() — chamado no QuizComplete, insere sequência no D1
12
+ * 2. runNurtureQueue() — chamado pelo Intelligence Agent (cron diário)
13
+ * envia as mensagens com send_at <= now()
14
+ */
15
+
16
+ import { Env, TrackPayload } from '../types.js';
17
+
18
+ // ── Tipos ─────────────────────────────────────────────────────────────────────
19
+
20
+ export type NurtureQualification = 'comprador' | 'interessado' | 'curioso' | 'perdido';
21
+
22
+ export interface NurtureStep {
23
+ delay_days: number;
24
+ channel: 'whatsapp' | 'email';
25
+ message: string; // suporta {{name}}, {{quiz_name}}, {{qualification}}
26
+ subject?: string; // apenas email
27
+ }
28
+
29
+ export interface NurtureResult {
30
+ scheduled: number;
31
+ skipped: string | null;
32
+ }
33
+
34
+ export interface NurtureRunResult {
35
+ processed: number;
36
+ sent: number;
37
+ failed: number;
38
+ excluded: number;
39
+ }
40
+
41
+ // ── Sequências por qualificação ───────────────────────────────────────────────
42
+ // Mensagens genéricas — o cliente personaliza via automation_rules no D1.
43
+ // O Nurture Engine usa estas como fallback quando não há regra cadastrada.
44
+
45
+ const NURTURE_SEQUENCES: Record<NurtureQualification, NurtureStep[]> = {
46
+ comprador: [
47
+ // comprador já dispara hot lead imediato no /track — sem sequência adicional aqui
48
+ ],
49
+ interessado: [
50
+ {
51
+ delay_days: 1,
52
+ channel: 'whatsapp',
53
+ message: 'Olá {{name}}! Vi que você completou nosso diagnóstico e ficou entre os mais qualificados. Posso te enviar mais detalhes sobre como podemos ajudar?',
54
+ },
55
+ {
56
+ delay_days: 3,
57
+ channel: 'whatsapp',
58
+ message: 'Oi {{name}}, tudo bem? Separei um conteúdo exclusivo baseado nas suas respostas no quiz "{{quiz_name}}". Posso compartilhar?',
59
+ },
60
+ {
61
+ delay_days: 7,
62
+ channel: 'whatsapp',
63
+ message: '{{name}}, última oportunidade esta semana! Muitos que fizeram o mesmo diagnóstico que você já estão obtendo resultados. Que tal conversarmos 15 minutos?',
64
+ },
65
+ ],
66
+ curioso: [
67
+ {
68
+ delay_days: 2,
69
+ channel: 'whatsapp',
70
+ message: 'Olá {{name}}! Você completou nosso diagnóstico. Preparei um material gratuito baseado no seu perfil. Posso enviar?',
71
+ },
72
+ {
73
+ delay_days: 5,
74
+ channel: 'whatsapp',
75
+ message: 'Oi {{name}}! Vi que você está pesquisando sobre o assunto. Tenho uma aula gratuita que pode te ajudar muito. Interesse?',
76
+ },
77
+ ],
78
+ perdido: [
79
+ // perdido não recebe mensagens — apenas é excluído do remarketing
80
+ ],
81
+ };
82
+
83
+ // ── scheduleNurture — agenda sequência após QuizComplete ─────────────────────
84
+
85
+ export async function scheduleNurture(
86
+ env: Env,
87
+ payload: TrackPayload,
88
+ qualification: NurtureQualification,
89
+ ): Promise<NurtureResult> {
90
+ if (!env.DB) return { scheduled: 0, skipped: 'DB não disponível' };
91
+
92
+ // perdido → só atualiza cohort_label para excluir do remarketing
93
+ if (qualification === 'perdido') {
94
+ if (payload.userId) {
95
+ try {
96
+ await env.DB.prepare(`
97
+ UPDATE user_profiles
98
+ SET cohort_label = 'excluded', updated_at = datetime('now')
99
+ WHERE user_id = ?
100
+ `).bind(payload.userId).run();
101
+ } catch { /* não-crítico */ }
102
+ }
103
+ return { scheduled: 0, skipped: 'perdido — excluído do remarketing' };
104
+ }
105
+
106
+ // comprador → contato imediato já gerenciado pelo hot lead em index.ts
107
+ if (qualification === 'comprador') {
108
+ return { scheduled: 0, skipped: 'comprador — contato imediato via hot lead' };
109
+ }
110
+
111
+ const steps = NURTURE_SEQUENCES[qualification];
112
+ if (!steps || steps.length === 0) {
113
+ return { scheduled: 0, skipped: `sem sequência para ${qualification}` };
114
+ }
115
+
116
+ // Verifica se já tem sequência ativa para este usuário (evita duplicar)
117
+ if (payload.userId) {
118
+ try {
119
+ const existing = await env.DB.prepare(`
120
+ SELECT id FROM nurture_sequences
121
+ WHERE user_id = ? AND status = 'pending'
122
+ LIMIT 1
123
+ `).bind(payload.userId).first();
124
+ if (existing) return { scheduled: 0, skipped: 'sequência já existe para este usuário' };
125
+ } catch { /* continua */ }
126
+ }
127
+
128
+ let scheduled = 0;
129
+ for (const step of steps) {
130
+ try {
131
+ const sendAt = new Date();
132
+ sendAt.setDate(sendAt.getDate() + step.delay_days);
133
+
134
+ await env.DB.prepare(`
135
+ INSERT INTO nurture_sequences (
136
+ user_id, qualification, delay_days, channel,
137
+ message, subject, send_at, status,
138
+ quiz_name, phone, email, first_name, created_at
139
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,datetime('now'))
140
+ `).bind(
141
+ payload.userId || null,
142
+ qualification,
143
+ step.delay_days,
144
+ step.channel,
145
+ step.message,
146
+ step.subject || null,
147
+ sendAt.toISOString().slice(0, 19).replace('T', ' '),
148
+ 'pending',
149
+ String((payload as any).quiz_name || ''),
150
+ payload.phone || null,
151
+ payload.email || null,
152
+ payload.firstName || null,
153
+ ).run();
154
+
155
+ scheduled++;
156
+ } catch (err: any) {
157
+ console.error('[Nurture] scheduleNurture insert error:', err?.message || String(err));
158
+ }
159
+ }
160
+
161
+ return { scheduled, skipped: null };
162
+ }
163
+
164
+ // ── runNurtureQueue — processa mensagens pendentes (chamado pelo cron) ────────
165
+
166
+ export async function runNurtureQueue(env: Env): Promise<NurtureRunResult> {
167
+ if (!env.DB) return { processed: 0, sent: 0, failed: 0, excluded: 0 };
168
+
169
+ const result: NurtureRunResult = { processed: 0, sent: 0, failed: 0, excluded: 0 };
170
+
171
+ try {
172
+ // Busca até 50 mensagens pendentes com send_at <= agora
173
+ const pending = await env.DB.prepare(`
174
+ SELECT * FROM nurture_sequences
175
+ WHERE status = 'pending'
176
+ AND send_at <= datetime('now')
177
+ ORDER BY send_at ASC
178
+ LIMIT 50
179
+ `).all();
180
+
181
+ const rows = (pending.results || []) as any[];
182
+ if (rows.length === 0) return result;
183
+
184
+ for (const row of rows) {
185
+ result.processed++;
186
+
187
+ // Interpola variáveis na mensagem
188
+ const vars: Record<string, string> = {
189
+ name: String(row.first_name || 'você'),
190
+ quiz_name: String(row.quiz_name || 'diagnóstico'),
191
+ qualification: String(row.qualification || ''),
192
+ };
193
+ const message = String(row.message || '').replace(/\{\{(\w+)\}\}/g, (_, k) => vars[k] ?? '');
194
+ const subject = row.subject ? String(row.subject).replace(/\{\{(\w+)\}\}/g, (_, k) => vars[k] ?? '') : null;
195
+
196
+ let success = false;
197
+
198
+ try {
199
+ if (row.channel === 'whatsapp' && row.phone && env.WHATSAPP_ACCESS_TOKEN && env.WHATSAPP_PHONE_NUMBER_ID) {
200
+ const digits = String(row.phone).replace(/\D/g, '');
201
+ const e164 = digits.startsWith('55') ? `+${digits}` : `+55${digits}`;
202
+
203
+ const res = await fetch(
204
+ `https://graph.facebook.com/v22.0/${env.WHATSAPP_PHONE_NUMBER_ID}/messages`,
205
+ {
206
+ method: 'POST',
207
+ headers: { 'Authorization': `Bearer ${env.WHATSAPP_ACCESS_TOKEN}`, 'Content-Type': 'application/json' },
208
+ body: JSON.stringify({
209
+ messaging_product: 'whatsapp',
210
+ recipient_type: 'individual',
211
+ to: e164,
212
+ type: 'text',
213
+ text: { body: message },
214
+ }),
215
+ }
216
+ );
217
+ success = res.ok;
218
+
219
+ } else if (row.channel === 'email' && row.email && env.RESEND_API_KEY) {
220
+ const res = await fetch('https://api.resend.com/emails', {
221
+ method: 'POST',
222
+ headers: { 'Authorization': `Bearer ${env.RESEND_API_KEY}`, 'Content-Type': 'application/json' },
223
+ body: JSON.stringify({
224
+ from: env.RESEND_FROM_EMAIL || 'noreply@cdp-edge.app',
225
+ to: [row.email],
226
+ subject: subject || `Olá, ${vars.name}!`,
227
+ html: `<p>${message.replace(/\n/g, '<br>')}</p>`,
228
+ }),
229
+ });
230
+ success = res.ok;
231
+ }
232
+ } catch (err: any) {
233
+ console.error(`[Nurture] dispatch error (row ${row.id}):`, err?.message || String(err));
234
+ }
235
+
236
+ // Atualiza status no D1
237
+ const newStatus = success ? 'sent' : 'failed';
238
+ const sentAt = success ? `datetime('now')` : 'NULL';
239
+ try {
240
+ await env.DB.prepare(`
241
+ UPDATE nurture_sequences
242
+ SET status = ?, sent_at = ${success ? "datetime('now')" : 'NULL'},
243
+ updated_at = datetime('now')
244
+ WHERE id = ?
245
+ `).bind(newStatus, row.id).run();
246
+ } catch { /* não-crítico */ }
247
+
248
+ if (success) result.sent++;
249
+ else result.failed++;
250
+ }
251
+
252
+ } catch (err: any) {
253
+ console.error('[Nurture] runNurtureQueue error:', err?.message || String(err));
254
+ }
255
+
256
+ return result;
257
+ }
@@ -0,0 +1,311 @@
1
+ /**
2
+ * CDP Edge — Utilities
3
+ * Funções puras sem dependências externas.
4
+ * Importadas por todos os outros módulos.
5
+ */
6
+
7
+ // ── Tipos ───────────────────────────────────────────────────────────────────────
8
+ export interface FunnelStageResult {
9
+ depth: string;
10
+ funnelDepth: string;
11
+ }
12
+
13
+ export interface MetaSignalWeights {
14
+ intent: number;
15
+ ltv: number;
16
+ dist: number;
17
+ }
18
+
19
+ export type DistanceBucket = 'very_close' | 'close' | 'nearby' | 'moderate' | 'far';
20
+
21
+ export type FunnelLevel = 'top' | 'mid' | 'bottom' | 'conversion' | 'unknown';
22
+
23
+ export type MetaSignalBucket = 'hot' | 'warm' | 'cold';
24
+
25
+ // ── CORS ──────────────────────────────────────────────────────────────────────
26
+ export function isAllowedOrigin(origin: string | null, siteDomain: string | null): boolean {
27
+ if (!origin || !siteDomain) return false;
28
+ return origin === `https://${siteDomain}`
29
+ || origin.endsWith(`.${siteDomain}`)
30
+ || origin === 'http://localhost:3000'
31
+ || origin === 'http://localhost:5173';
32
+ }
33
+
34
+ export function corsHeaders(origin: string | null, siteDomain: string | null): Record<string, string> {
35
+ const allowed = isAllowedOrigin(origin, siteDomain) ? origin : (siteDomain ? `https://${siteDomain}` : '*');
36
+ return {
37
+ 'Access-Control-Allow-Origin': allowed || '*',
38
+ 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
39
+ 'Access-Control-Allow-Headers': 'Content-Type, X-Requested-With',
40
+ 'Access-Control-Max-Age': '86400',
41
+ };
42
+ }
43
+
44
+ // ── SHA-256 via WebCrypto (obrigatório no Cloudflare Workers) ─────────────────
45
+ export async function sha256(value: string | null | undefined): Promise<string | undefined> {
46
+ if (!value) return undefined;
47
+ const clean = String(value).toLowerCase().trim();
48
+ if (!clean) return undefined;
49
+ const buf = await crypto.subtle.digest(
50
+ 'SHA-256',
51
+ new TextEncoder().encode(clean)
52
+ );
53
+ return Array.from(new Uint8Array(buf))
54
+ .map(b => b.toString(16).padStart(2, '0'))
55
+ .join('');
56
+ }
57
+
58
+ // ── Normalização de telefone → somente dígitos + DDI 55 ──────────────────────
59
+ export function normalizePhone(phone: string | null | undefined): string | undefined {
60
+ if (!phone) return undefined;
61
+ let digits = String(phone).replace(/\D/g, '');
62
+ if (digits.length === 11 && !digits.startsWith('55')) digits = '55' + digits;
63
+ if (digits.length === 10 && !digits.startsWith('55')) digits = '55' + digits;
64
+ return digits.length >= 10 ? digits : undefined;
65
+ }
66
+
67
+ // ── Normalização de cidade → lowercase sem acentos ────────────────────────────
68
+ export function normalizeCity(city: string | null | undefined): string | undefined {
69
+ if (!city) return undefined;
70
+ return String(city)
71
+ .toLowerCase()
72
+ .normalize('NFD')
73
+ .replace(/[\u0300-\u036f]/g, '')
74
+ .replace(/[^a-z0-9]/g, '');
75
+ }
76
+
77
+ // ── Parse seguro de JSON armazenado como TEXT no D1 ───────────────────────────
78
+ export function tryParseJson<T = any>(str: string | null, fallback?: T): T | null {
79
+ if (!str) return fallback !== undefined ? fallback : null;
80
+ try { return JSON.parse(str); } catch { return fallback !== undefined ? fallback : null; }
81
+ }
82
+
83
+ // ── Mapa Meta → GA4 event names ───────────────────────────────────────────────
84
+ export const META_TO_GA4: Record<string, string> = {
85
+ PageView: 'page_view',
86
+ ViewContent: 'view_item',
87
+ Lead: 'generate_lead',
88
+ Contact: 'generate_lead',
89
+ Schedule: 'generate_lead',
90
+ InitiateCheckout: 'begin_checkout',
91
+ AddToCart: 'add_to_cart',
92
+ AddPaymentInfo: 'add_payment_info',
93
+ Purchase: 'purchase',
94
+ CompleteRegistration: 'sign_up',
95
+ Subscribe: 'subscribe',
96
+ StartTrial: 'start_trial',
97
+ Search: 'search',
98
+ AddToWishlist: 'add_to_wishlist',
99
+ };
100
+
101
+ // ── Lista canônica de eventos válidos (19 eventos) ────────────────────────────
102
+ export const VALID_EVENT_NAMES = new Set([
103
+ 'PageView','ViewContent','Lead','Purchase','InitiateCheckout',
104
+ 'AddToCart','CompleteRegistration','Contact','Schedule',
105
+ 'StartTrial','Subscribe','SubmitApplication','Search',
106
+ 'video_start','video_25','video_50','video_75','video_complete',
107
+ // Imóveis — intenção de visita física, financiamento e favoritar
108
+ 'FindLocation','CustomizeProduct','AddToWishlist',
109
+ // Quiz Funnel (Fase 6)
110
+ 'QuizStart','QuizAnswer','QuizComplete',
111
+ ]);
112
+
113
+ // ── Taxonomia de funil (funnel_stage → profundidade semântica) ────────────────
114
+ // Fonte de verdade para interpretar funnel_stage em qualquer ponto do sistema.
115
+ export const FUNNEL_TAXONOMY = {
116
+ top: ['scroll_50', 'time_30s', 'page_view', 'gallery_view', 'AddToWishlist'],
117
+ mid: ['map_view', 'gallery_click', 'price_hover', 'time_3min', 'FindLocation'],
118
+ bottom: ['route_click', 'whatsapp_click', 'cta_hover', 'CustomizeProduct'],
119
+ conversion: ['schedule_confirmed', 'lead_form', 'purchase', 'visit_booked'],
120
+ };
121
+
122
+ // Índice invertido: funnel_stage → depth (construído uma vez, zero custo em runtime)
123
+ const _STAGE_TO_DEPTH: Record<string, FunnelLevel> = Object.entries(FUNNEL_TAXONOMY).reduce((acc, [depth, stages]) => {
124
+ stages.forEach(s => { acc[s] = depth as FunnelLevel; });
125
+ return acc;
126
+ }, {} as Record<string, FunnelLevel>);
127
+
128
+ /**
129
+ * Resolve funnel_stage em funnelDepth semântico.
130
+ * bottom_intent = intenção forte (route_click, whatsapp_click)
131
+ * bottom_conversion = ação confirmada (schedule_confirmed, lead_form)
132
+ */
133
+ export function resolveFunnelStage(funnel_stage: string | null | undefined): FunnelStageResult {
134
+ const depth = _STAGE_TO_DEPTH[funnel_stage || ''] || 'unknown';
135
+ const funnelDepth = depth === 'conversion' ? 'bottom_conversion'
136
+ : depth === 'bottom' ? 'bottom_intent'
137
+ : depth;
138
+ return { depth, funnelDepth };
139
+ }
140
+
141
+ // ── Normalização de intent_score → 0.0–1.0 ───────────────────────────────────
142
+ // Aceita: string ('high'/'medium'/'low'), numérico 0-1 ou numérico 0-100
143
+ const _INTENT_STRING_MAP: Record<string, number> = { high: 0.92, medium: 0.65, low: 0.30 };
144
+
145
+ export function resolveIntentScore(value: string | number | null | undefined): number | null {
146
+ if (value === null || value === undefined) return null;
147
+ if (typeof value === 'string') return _INTENT_STRING_MAP[value.toLowerCase()] ?? null;
148
+ const num = parseFloat(String(value));
149
+ if (isNaN(num)) return null;
150
+ const normalized = num > 1 ? num / 100 : num; // escala 0-100 → 0-1
151
+ return Math.min(1, Math.max(0, Math.round(normalized * 100) / 100));
152
+ }
153
+
154
+ /**
155
+ * Distância (distanceBucket) → peso numérico para meta_signal.
156
+ * very_close=1.0 ... far=0.1 ... sem dado=0.3 (neutro)
157
+ */
158
+ export function distanceBucketWeight(bucket: string | null | undefined): number {
159
+ const map: Record<DistanceBucket, number> = { very_close: 1.0, close: 0.75, nearby: 0.5, moderate: 0.25, far: 0.1 };
160
+ return map[bucket as DistanceBucket] ?? 0.3;
161
+ }
162
+
163
+ /**
164
+ * Pesos dinâmicos do meta_signal por profundidade de funil.
165
+ * Fundo: comportamento pesa mais (intent + dist).
166
+ * Topo: perfil pesa mais (ltv).
167
+ * Default (mid/unknown): balanceado.
168
+ */
169
+ export function computeMetaSignalWeights(funnelLevel: FunnelLevel | string | null | undefined): MetaSignalWeights {
170
+ if (funnelLevel === 'bottom' || funnelLevel === 'conversion') {
171
+ return { intent: 0.5, ltv: 0.2, dist: 0.3 };
172
+ }
173
+ if (funnelLevel === 'top') {
174
+ return { intent: 0.2, ltv: 0.6, dist: 0.2 };
175
+ }
176
+ return { intent: 0.4, ltv: 0.4, dist: 0.2 };
177
+ }
178
+
179
+ /**
180
+ * Quantiza meta_signal contínuo em bucket legível.
181
+ * Usado em criação de públicos e leitura de BI.
182
+ */
183
+ export function metaSignalBucket(score: number | null | undefined): MetaSignalBucket {
184
+ if (!score) return 'cold';
185
+ if (score >= 0.8) return 'hot';
186
+ if (score >= 0.6) return 'warm';
187
+ return 'cold';
188
+ }
189
+
190
+ // ── Input Validation & Sanitization — Segurança contra XSS/Injection ────────
191
+
192
+ /**
193
+ * Valida formato de email (basic RFC-compliant)
194
+ */
195
+ export function isValidEmail(email: string | null | undefined): boolean {
196
+ if (!email || typeof email !== 'string') return false;
197
+ const trimmed = email.trim();
198
+ if (trimmed.length > 256) return false; // Limite razoável
199
+ const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
200
+ return emailRegex.test(trimmed);
201
+ }
202
+
203
+ /**
204
+ * Sanitiza string contra XSS/HTML injection
205
+ * Remove tags HTML, scripts, e caracteres perigosos
206
+ */
207
+ export function sanitizeString(input: string | null | undefined, maxLength: number = 512): string | null {
208
+ if (!input || typeof input !== 'string') return null;
209
+ let sanitized = String(input).trim();
210
+
211
+ // Remove HTML tags
212
+ sanitized = sanitized.replace(/<[^>]*>/g, '');
213
+
214
+ // Remove JavaScript event handlers
215
+ sanitized = sanitized.replace(/on\w+\s*=/gi, '');
216
+
217
+ // Remove javascript: protocol
218
+ sanitized = sanitized.replace(/javascript:/gi, '');
219
+
220
+ // Remove caracteres perigosos
221
+ sanitized = sanitized.replace(/[<>\"'`]/g, '');
222
+
223
+ // Remove caracteres Unicode perigosos
224
+ sanitized = sanitized.replace(/[\x00-\x1F\x7F]/g, '');
225
+
226
+ // Limita comprimento
227
+ if (sanitized.length > maxLength) {
228
+ sanitized = sanitized.substring(0, maxLength);
229
+ }
230
+
231
+ return sanitized.length > 0 ? sanitized : null;
232
+ }
233
+
234
+ /**
235
+ * Valida e sanitiza URL (para pageUrl)
236
+ */
237
+ export function isValidUrl(url: string | null | undefined): boolean {
238
+ if (!url || typeof url !== 'string') return false;
239
+ const trimmed = url.trim();
240
+ if (trimmed.length > 2048) return false; // Limite razoável
241
+ try {
242
+ const parsed = new URL(trimmed);
243
+ return ['http:', 'https:'].includes(parsed.protocol);
244
+ } catch {
245
+ return false;
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Valida formato de CPF (11 dígitos)
251
+ */
252
+ export function isValidCPF(cpf: string | null | undefined): boolean {
253
+ if (!cpf || typeof cpf !== 'string') return false;
254
+ const cleaned = cpf.replace(/\D/g, '');
255
+ return cleaned.length === 11 && /^\d+$/.test(cleaned);
256
+ }
257
+
258
+ /**
259
+ * Valida formato de CNPJ (14 dígitos)
260
+ */
261
+ export function isValidCNPJ(cnpj: string | null | undefined): boolean {
262
+ if (!cnpj || typeof cnpj !== 'string') return false;
263
+ const cleaned = cnpj.replace(/\D/g, '');
264
+ return cleaned.length === 14 && /^\d+$/.test(cleaned);
265
+ }
266
+
267
+ /**
268
+ * Valida formato de valor numérico (para value em Purchase)
269
+ */
270
+ export function isValidValue(value: number | null | undefined): boolean {
271
+ if (value === null || value === undefined) return true; // Valor opcional
272
+ const num = Number(value);
273
+ return !isNaN(num) && num >= 0 && num <= 9_999_999;
274
+ }
275
+
276
+ /**
277
+ * Valida moeda (currency field)
278
+ */
279
+ export function isValidCurrency(currency: string | null | undefined): boolean {
280
+ if (!currency || typeof currency !== 'string') return true; // Opcional
281
+ const trimmed = currency.trim().toUpperCase();
282
+ const validCurrencies = ['BRL', 'USD', 'EUR', 'GBP', 'CAD', 'AUD', 'JPY', 'CHF'];
283
+ return trimmed.length === 3 && validCurrencies.includes(trimmed);
284
+ }
285
+
286
+ /**
287
+ * Sanitiza array de strings (para contentIds, etc.)
288
+ */
289
+ export function sanitizeStringArray(input: string[] | null | undefined, maxLength: number = 512): string[] | null {
290
+ if (!input || !Array.isArray(input)) return null;
291
+ const sanitized = input
292
+ .map(item => sanitizeString(item, maxLength))
293
+ .filter(item => item !== null) as string[];
294
+ return sanitized.length > 0 ? sanitized : null;
295
+ }
296
+
297
+ /**
298
+ * Valida UTM parameters (utmSource, utmMedium, utmCampaign, utmContent, utmTerm)
299
+ */
300
+ export function isValidUTM(param: string | null | undefined, paramType: string): boolean {
301
+ if (!param || typeof param !== 'string') return true; // Opcional
302
+ const trimmed = param.trim();
303
+ const maxLength = paramType === 'utm_source' ? 100 : 200;
304
+
305
+ if (trimmed.length > maxLength) return false;
306
+
307
+ // Verifica caracteres perigosos
308
+ const dangerousPatterns = ['<script', 'javascript:', 'onload=', 'onerror=', 'onclick='];
309
+ const lowerCase = trimmed.toLowerCase();
310
+ return !dangerousPatterns.some(pattern => lowerCase.includes(pattern));
311
+ }