cdp-edge 2.3.1 → 2.3.6

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.
@@ -184,3 +184,126 @@ export function metaSignalBucket(score: number | null | undefined): MetaSignalBu
184
184
  if (score >= 0.6) return 'warm';
185
185
  return 'cold';
186
186
  }
187
+
188
+ // ── Input Validation & Sanitization — Segurança contra XSS/Injection ────────
189
+
190
+ /**
191
+ * Valida formato de email (basic RFC-compliant)
192
+ */
193
+ export function isValidEmail(email: string | null | undefined): boolean {
194
+ if (!email || typeof email !== 'string') return false;
195
+ const trimmed = email.trim();
196
+ if (trimmed.length > 256) return false; // Limite razoável
197
+ const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
198
+ return emailRegex.test(trimmed);
199
+ }
200
+
201
+ /**
202
+ * Sanitiza string contra XSS/HTML injection
203
+ * Remove tags HTML, scripts, e caracteres perigosos
204
+ */
205
+ export function sanitizeString(input: string | null | undefined, maxLength: number = 512): string | null {
206
+ if (!input || typeof input !== 'string') return null;
207
+ let sanitized = String(input).trim();
208
+
209
+ // Remove HTML tags
210
+ sanitized = sanitized.replace(/<[^>]*>/g, '');
211
+
212
+ // Remove JavaScript event handlers
213
+ sanitized = sanitized.replace(/on\w+\s*=/gi, '');
214
+
215
+ // Remove javascript: protocol
216
+ sanitized = sanitized.replace(/javascript:/gi, '');
217
+
218
+ // Remove caracteres perigosos
219
+ sanitized = sanitized.replace(/[<>\"'`]/g, '');
220
+
221
+ // Remove caracteres Unicode perigosos
222
+ sanitized = sanitized.replace(/[\x00-\x1F\x7F]/g, '');
223
+
224
+ // Limita comprimento
225
+ if (sanitized.length > maxLength) {
226
+ sanitized = sanitized.substring(0, maxLength);
227
+ }
228
+
229
+ return sanitized.length > 0 ? sanitized : null;
230
+ }
231
+
232
+ /**
233
+ * Valida e sanitiza URL (para pageUrl)
234
+ */
235
+ export function isValidUrl(url: string | null | undefined): boolean {
236
+ if (!url || typeof url !== 'string') return false;
237
+ const trimmed = url.trim();
238
+ if (trimmed.length > 2048) return false; // Limite razoável
239
+ try {
240
+ const parsed = new URL(trimmed);
241
+ return ['http:', 'https:'].includes(parsed.protocol);
242
+ } catch {
243
+ return false;
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Valida formato de CPF (11 dígitos)
249
+ */
250
+ export function isValidCPF(cpf: string | null | undefined): boolean {
251
+ if (!cpf || typeof cpf !== 'string') return false;
252
+ const cleaned = cpf.replace(/\D/g, '');
253
+ return cleaned.length === 11 && /^\d+$/.test(cleaned);
254
+ }
255
+
256
+ /**
257
+ * Valida formato de CNPJ (14 dígitos)
258
+ */
259
+ export function isValidCNPJ(cnpj: string | null | undefined): boolean {
260
+ if (!cnpj || typeof cnpj !== 'string') return false;
261
+ const cleaned = cnpj.replace(/\D/g, '');
262
+ return cleaned.length === 14 && /^\d+$/.test(cleaned);
263
+ }
264
+
265
+ /**
266
+ * Valida formato de valor numérico (para value em Purchase)
267
+ */
268
+ export function isValidValue(value: number | null | undefined): boolean {
269
+ if (value === null || value === undefined) return true; // Valor opcional
270
+ const num = Number(value);
271
+ return !isNaN(num) && num >= 0 && num <= 9_999_999;
272
+ }
273
+
274
+ /**
275
+ * Valida moeda (currency field)
276
+ */
277
+ export function isValidCurrency(currency: string | null | undefined): boolean {
278
+ if (!currency || typeof currency !== 'string') return true; // Opcional
279
+ const trimmed = currency.trim().toUpperCase();
280
+ const validCurrencies = ['BRL', 'USD', 'EUR', 'GBP', 'CAD', 'AUD', 'JPY', 'CHF'];
281
+ return trimmed.length === 3 && validCurrencies.includes(trimmed);
282
+ }
283
+
284
+ /**
285
+ * Sanitiza array de strings (para contentIds, etc.)
286
+ */
287
+ export function sanitizeStringArray(input: string[] | null | undefined, maxLength: number = 512): string[] | null {
288
+ if (!input || !Array.isArray(input)) return null;
289
+ const sanitized = input
290
+ .map(item => sanitizeString(item, maxLength))
291
+ .filter(item => item !== null) as string[];
292
+ return sanitized.length > 0 ? sanitized : null;
293
+ }
294
+
295
+ /**
296
+ * Valida UTM parameters (utmSource, utmMedium, utmCampaign, utmContent, utmTerm)
297
+ */
298
+ export function isValidUTM(param: string | null | undefined, paramType: string): boolean {
299
+ if (!param || typeof param !== 'string') return true; // Opcional
300
+ const trimmed = param.trim();
301
+ const maxLength = paramType === 'utm_source' ? 100 : 200;
302
+
303
+ if (trimmed.length > maxLength) return false;
304
+
305
+ // Verifica caracteres perigosos
306
+ const dangerousPatterns = ['<script', 'javascript:', 'onload=', 'onerror=', 'onclick='];
307
+ const lowerCase = trimmed.toLowerCase();
308
+ return !dangerousPatterns.some(pattern => lowerCase.includes(pattern));
309
+ }
@@ -0,0 +1,231 @@
1
+ /**
2
+ * UTM Enricher Module
3
+ * Obfusca/desobfusca UTMs sensíveis (valores de produto)
4
+ * Integração com Agente UTM
5
+ */
6
+
7
+ // ============================================================================
8
+ // Constants & Config
9
+ // ============================================================================
10
+
11
+ const UTM_SALT = 'CDP_EDGE_UTM_SALT';
12
+ const HASH_TRUNCATE_LENGTH = 8;
13
+
14
+ // Obfuscação: SHA256(original + salt) → truncate(8)
15
+ // Isso garante: mesmo valor → mesmo hash, mas irreversível sem o mapeamento
16
+
17
+ // ============================================================================
18
+ // Types
19
+ // ============================================================================
20
+
21
+ export interface UTMMapping {
22
+ obfuscated: string; // Hash truncado (ex: "8a3f1d2b")
23
+ original: string; // Valor real (ex: "700k-1M")
24
+ category: string; // "imovel", "automotivo", etc
25
+ pixel_audience?: string; // ID da custom audience Meta
26
+ platform_specific?: {
27
+ meta?: { custom_audience_id?: string };
28
+ tiktok?: { pixel_id?: string };
29
+ ga4?: { event_parameter?: string };
30
+ };
31
+ }
32
+
33
+ export interface UTMMappingConfig {
34
+ method: 'sha256';
35
+ salt: string;
36
+ truncated_length: number;
37
+ mappings: UTMMapping[];
38
+ }
39
+
40
+ export interface EnrichedUTM {
41
+ source?: string;
42
+ medium?: string;
43
+ campaign?: string;
44
+ content?: string;
45
+ faixa_obfuscada?: string; // Hash da faixa de valor
46
+ faixa_real?: string; // Valor real (de-obfuscado)
47
+ faixa_category?: string; // Categoria do produto
48
+ product_id_obfuscated?: string;
49
+ product_id_real?: string;
50
+ }
51
+
52
+ // ============================================================================
53
+ // Core Functions
54
+ // ============================================================================
55
+
56
+ /**
57
+ * Obfusca um valor sensível usando SHA256 + truncate
58
+ * @param value - Valor a ser obfuscado (ex: "700k-1M")
59
+ * @returns Hash truncado de 8 caracteres
60
+ */
61
+ export function obfuscateValue(value: string): string {
62
+ // sha256(value + salt) → truncate(8)
63
+ const hash = sha256(`${value}${UTM_SALT}`);
64
+ return hash.substring(0, HASH_TRUNCATE_LENGTH);
65
+ }
66
+
67
+ /**
68
+ * Verifica se um hash é válido (8 chars hex)
69
+ */
70
+ export function isValidObfuscatedHash(hash: string): boolean {
71
+ return /^[a-f0-9]{8}$/.test(hash);
72
+ }
73
+
74
+ /**
75
+ * Desobfusca um valor usando o mapeamento
76
+ * @param obfuscated - Hash obfuscado
77
+ * @param mappings - Mapeamento de UTM (do config/utm-mapping.json)
78
+ * @returns UTM com valor real ou undefined se não encontrado
79
+ */
80
+ export function deobfuscateValue(
81
+ obfuscated: string,
82
+ mappings: UTMMapping[]
83
+ ): UTMMapping | undefined {
84
+ return mappings.find(m => m.obfuscated === obfuscated);
85
+ }
86
+
87
+ /**
88
+ * Enrich payload com UTMs, desobfuscando valores sensíveis
89
+ */
90
+ export function enrichPayloadWithUTM(
91
+ payload: any,
92
+ utms: Record<string, string>,
93
+ mappings: UTMMapping[]
94
+ ): { enriched: any; faixa?: UTMMapping } {
95
+ const enriched = { ...payload };
96
+ let faixa: UTMMapping | undefined;
97
+
98
+ // Desobfuscar faixa de valor
99
+ if (utms.faixa_obfuscada || utms.utm_faixa) {
100
+ const faixaHash = utms.faixa_obfuscada || utms.utm_faixa;
101
+ if (isValidObfuscatedHash(faixaHash)) {
102
+ faixa = deobfuscateValue(faixaHash, mappings);
103
+ if (faixa) {
104
+ enriched.faixa_real = faixa.original;
105
+ enriched.faixa_category = faixa.category;
106
+ enriched.pixel_audience = faixa.pixel_audience;
107
+ }
108
+ }
109
+ }
110
+
111
+ // Extrair UTMs padrão
112
+ enriched.utm_source = utms.utm_source || utms.source;
113
+ enriched.utm_medium = utms.utm_medium || utms.medium;
114
+ enriched.utm_campaign = utms.utm_campaign || utms.campaign;
115
+ enriched.utm_content = utms.utm_content || utms.content;
116
+
117
+ return { enriched, faixa };
118
+ }
119
+
120
+ /**
121
+ * Gera UTM obfuscada para uma faixa de valor
122
+ * @param range - Faixa real (ex: "700k-1M")
123
+ * @param category - Categoria (ex: "imovel")
124
+ * @returns Object com hash obfuscado
125
+ */
126
+ export function generateObfuscatedUTM(range: string, category: string) {
127
+ const obfuscated = obfuscateValue(range);
128
+ return {
129
+ utm_faixa: obfuscated,
130
+ utm_campaign: `${category}_${obfuscated}`,
131
+ original_range: range,
132
+ hash: obfuscated
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Cria um novo mapeamento de UTM
138
+ */
139
+ export function createUTMMapping(
140
+ original: string,
141
+ category: string,
142
+ platform_specific?: any
143
+ ): UTMMapping {
144
+ return {
145
+ obfuscated: obfuscateValue(original),
146
+ original,
147
+ category,
148
+ platform_specific
149
+ };
150
+ }
151
+
152
+ // ============================================================================
153
+ // Integration Functions (para uso no Worker)
154
+ // ============================================================================
155
+
156
+ /**
157
+ * Verifica se payload tem UTMs de segmentação
158
+ */
159
+ export function hasSegmentationUTM(utms: Record<string, string>): boolean {
160
+ return !!(
161
+ utms.faixa_obfuscada ||
162
+ utms.utm_faixa ||
163
+ (utms.utm_campaign && isValidObfuscatedHash(
164
+ utms.utm_campaign.split('_').pop() || ''
165
+ ))
166
+ );
167
+ }
168
+
169
+ /**
170
+ * Extrai faixa de valor do utm_campaign (pattern: category_hash)
171
+ */
172
+ export function extractFaixaFromCampaign(
173
+ campaign: string
174
+ ): string | null {
175
+ const parts = campaign.split('_');
176
+ const hash = parts.pop();
177
+ if (hash && isValidObfuscatedHash(hash)) {
178
+ return hash;
179
+ }
180
+ return null;
181
+ }
182
+
183
+ /**
184
+ * Para Meta CAPI: adiciona segmentação ao external_id
185
+ */
186
+ export function addSegmentationToExternalId(
187
+ cdp_uid: string,
188
+ faixa: UTMMapping
189
+ ): string {
190
+ return `${cdp_uid}_${faixa.obfuscated}`;
191
+ }
192
+
193
+ /**
194
+ * Para GA4: cria custom parameter segmentado
195
+ */
196
+ export function createSegmentationCustomParameter(faixa: UTMMapping) {
197
+ return {
198
+ 'custom_faixa_categoria': faixa.category,
199
+ 'custom_faixa_obfuscada': faixa.obfuscated,
200
+ 'custom_faixa_audience': faixa.pixel_audience || 'UNKNOWN'
201
+ };
202
+ }
203
+
204
+ // ============================================================================
205
+ // Import de sha256 (reutilizar de utils.ts)
206
+ // ============================================================================
207
+
208
+ function sha256(message: string): string {
209
+ // Importado de utils.ts - implementação real do SHA256
210
+ // Aqui simulamos para o exemplo:
211
+ const crypto = require('crypto');
212
+ return crypto.createHash('sha256').update(message).digest('hex');
213
+ }
214
+
215
+ // ============================================================================
216
+ // Export
217
+ // ============================================================================
218
+
219
+ export const UTM_ENRICHER_VERSION = '1.0.0';
220
+
221
+ export default {
222
+ obfuscateValue,
223
+ deobfuscateValue,
224
+ enrichPayloadWithUTM,
225
+ generateObfuscatedUTM,
226
+ createUTMMapping,
227
+ hasSegmentationUTM,
228
+ extractFaixaFromCampaign,
229
+ addSegmentationToExternalId,
230
+ createSegmentationCustomParameter
231
+ };
@@ -0,0 +1,80 @@
1
+ -- ============================================================================
2
+ -- SCHEMA UTM OBFUSCATION — Fase de Segmentação de Valor
3
+ -- ============================================================================
4
+ -- Este schema estende o schema.sql existente com colunas para UTMs obfuscadas
5
+ -- e uma tabela de mapeamento para de-obfuscação no runtime.
6
+ --
7
+ -- Como funciona:
8
+ -- 1. utm_mappings: mapeia hash obfuscado → valor real (configuração)
9
+ -- 2. leads: adiciona colunas faixa_real, faixa_category (segmentação)
10
+ -- 3. dispatch: payload enriquecido com faixa de-obfuscada para Meta CAPI
11
+ --
12
+ -- ============================================================================
13
+ -- TABELA: utm_mappings (configuração de segmentação)
14
+ -- ============================================================================
15
+ CREATE TABLE IF NOT EXISTS utm_mappings (
16
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
17
+ obfuscated_hash TEXT NOT NULL UNIQUE, -- ex: "8a3f1d2b" (hash truncado de 8 chars)
18
+ original_value TEXT NOT NULL, -- ex: "700k-1M" (valor real)
19
+ category TEXT NOT NULL, -- ex: "imovel", "automotivo", "curso"
20
+ pixel_audience TEXT, -- ex: "AUDIENCE_MID" (Meta custom audience)
21
+ platform_specific TEXT, -- JSON com IDs específicos por plataforma
22
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
23
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
24
+ );
25
+
26
+ -- ============================================================================
27
+ -- ÍNDICES: utm_mappings
28
+ -- ============================================================================
29
+ CREATE INDEX IF NOT EXISTS idx_utm_obfuscated ON utm_mappings(obfuscated_hash);
30
+ CREATE INDEX IF NOT EXISTS idx_utm_category ON utm_mappings(category);
31
+
32
+ -- ============================================================================
33
+ -- ALTER: leads (adicionar colunas de segmentação)
34
+ -- ============================================================================
35
+ ALTER TABLE leads ADD COLUMN IF NOT EXISTS faixa_obfuscada TEXT; -- Hash da faixa de valor (vem da URL)
36
+ ALTER TABLE leads ADD COLUMN IF NOT EXISTS faixa_real TEXT; -- Valor real de-obfuscado (ex: "700k-1M")
37
+ ALTER TABLE leads ADD COLUMN IF NOT EXISTS faixa_category TEXT; -- Categoria do produto (ex: "imovel")
38
+
39
+ -- ============================================================================
40
+ -- ÍNDICES: leads (novas colunas para segmentação)
41
+ -- ============================================================================
42
+ CREATE INDEX IF NOT EXISTS idx_leads_faixa_real ON leads(faixa_real);
43
+ CREATE INDEX IF NOT EXISTS idx_leads_faixa_category ON leads(faixa_category);
44
+ CREATE INDEX IF NOT EXISTS idx_leads_faixa_obfuscada ON leads(faixa_obfuscada);
45
+
46
+ -- ============================================================================
47
+ -- VIEW: leads_segmented (para queries de segmentação)
48
+ -- ============================================================================
49
+ CREATE VIEW IF NOT EXISTS leads_segmented AS
50
+ SELECT
51
+ l.id,
52
+ l.user_id,
53
+ l.event,
54
+ l.event_id,
55
+ l.email,
56
+ l.phone,
57
+ l.city,
58
+ l.state,
59
+ l.faixa_obfuscada,
60
+ l.faixa_real,
61
+ l.faixa_category,
62
+ l.created_at,
63
+ l.value,
64
+ l.intent_score,
65
+ l.ltv_class,
66
+ -- Meta CAPI: custom audience de-obfuscada
67
+ u.pixel_audience AS meta_custom_audience
68
+ FROM leads l
69
+ LEFT JOIN utm_mappings u ON l.faixa_obfuscada = u.obfuscated_hash;
70
+
71
+ -- ============================================================================
72
+ -- EXEMPLO: Query para exportar leads por faixa de valor (para Meta Custom Audience)
73
+ -- ============================================================================
74
+ -- SELECT email, phone, city, state, faixa_real, meta_custom_audience
75
+ -- FROM leads_segmented
76
+ -- WHERE faixa_category = 'imovel' AND faixa_real = '700k-1M'
77
+ -- AND created_at >= datetime('now', '-30 days');
78
+ --
79
+ -- Isso gera um CSV pronto para upload como Custom Audience na Meta.
80
+ -- ============================================================================
@@ -3,7 +3,7 @@
3
3
  * Tipos para o Cloudflare Worker e bindings
4
4
  */
5
5
 
6
- import { D1Database, KVNamespace, R2Bucket } from '@cloudflare/workers-types';
6
+ import { D1Database, KVNamespace, Queue, R2Bucket } from '@cloudflare/workers-types';
7
7
 
8
8
  // ── Environment Bindings ─────────────────────────────────────────────────────
9
9
  export interface Env {
@@ -22,6 +22,9 @@ export interface Env {
22
22
  // Rate Limiter
23
23
  RATE_LIMITER?: any;
24
24
 
25
+ // Queue — Retry de eventos com falha de rede
26
+ RETRY_QUEUE?: Queue<QueueMessage>;
27
+
25
28
  // Public Variables
26
29
  META_PIXEL_ID?: string;
27
30
  GA4_MEASUREMENT_ID?: string;