cdp-edge 1.23.3 → 1.24.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 (24) hide show
  1. package/README.md +39 -2
  2. package/bin/cdp-edge.js +10 -1
  3. package/contracts/types.ts +81 -0
  4. package/dist/commands/install.js +6 -1
  5. package/docs/whatsapp-ctwa.md +3 -2
  6. package/package.json +7 -4
  7. package/server-edge-tracker/{index.js → index.ts} +91 -82
  8. package/server-edge-tracker/modules/{db.js → db.ts} +116 -76
  9. package/server-edge-tracker/modules/dispatch/{ga4.js → ga4.ts} +12 -10
  10. package/server-edge-tracker/modules/dispatch/{meta.js → meta.ts} +35 -28
  11. package/server-edge-tracker/modules/dispatch/{platforms.js → platforms.ts} +58 -56
  12. package/server-edge-tracker/modules/dispatch/{tiktok.js → tiktok.ts} +22 -20
  13. package/server-edge-tracker/modules/dispatch/{whatsapp.js → whatsapp.ts} +59 -25
  14. package/server-edge-tracker/modules/{intelligence.js → intelligence.ts} +175 -60
  15. package/server-edge-tracker/modules/ml/{bidding.js → bidding.ts} +37 -35
  16. package/server-edge-tracker/modules/ml/{fraud.js → fraud.ts} +48 -40
  17. package/server-edge-tracker/modules/ml/{logistic.js → logistic.ts} +44 -19
  18. package/server-edge-tracker/modules/ml/{ltv.js → ltv.ts} +135 -90
  19. package/server-edge-tracker/modules/ml/{matchquality.js → matchquality.ts} +70 -26
  20. package/server-edge-tracker/modules/ml/{segmentation.js → segmentation.ts} +109 -48
  21. package/server-edge-tracker/modules/{utils.js → utils.ts} +41 -22
  22. package/server-edge-tracker/types.ts +251 -0
  23. package/server-edge-tracker/wrangler.toml +8 -8
  24. package/docs/PixelBuilder-Documentacao-Completa (2).docx +0 -0
@@ -5,9 +5,11 @@
5
5
 
6
6
  import { sha256, normalizePhone } from '../utils.js';
7
7
  import { logApiFailure } from '../db.js';
8
+ import { Env, TrackPayload } from '../../types.js';
9
+ import { ExecutionContext } from '@cloudflare/workers-types';
8
10
 
9
11
  // ── Pinterest Conversions API v5 ──────────────────────────────────────────────
10
- export async function sendPinterestCapi(env, eventName, payload, request, ctx) {
12
+ export async function sendPinterestCapi(env: Env, eventName: string, payload: TrackPayload, request: Request | null, ctx: ExecutionContext | null): Promise<any> {
11
13
  if (!env.PINTEREST_ACCESS_TOKEN || !env.PINTEREST_AD_ACCOUNT_ID) {
12
14
  return { skipped: 'Pinterest credentials not set' };
13
15
  }
@@ -15,19 +17,19 @@ export async function sendPinterestCapi(env, eventName, payload, request, ctx) {
15
17
  const { email, phone, userId, eventId, pageUrl, value, currency, contentIds, contentName } = payload;
16
18
  const phoneNorm = normalizePhone(phone);
17
19
 
18
- const pinterestEventMap = {
20
+ const pinterestEventMap: Record<string, string> = {
19
21
  PageView: 'pagevisit', ViewContent: 'pagevisit', Lead: 'lead', Purchase: 'checkout',
20
22
  AddToCart: 'addtocart', InitiateCheckout: 'checkout', CompleteRegistration: 'signup',
21
23
  Search: 'search', Contact: 'lead',
22
24
  };
23
25
  const pEvent = pinterestEventMap[eventName] || 'custom';
24
26
 
25
- const userData = {
26
- ...(email && { em: [await sha256(email)] }),
27
- ...(phoneNorm && { ph: [await sha256(phoneNorm)] }),
28
- ...(userId && { external_id: [await sha256(String(userId))] }),
29
- client_ip_address: request.headers.get('CF-Connecting-IP') || '',
30
- client_user_agent: request.headers.get('User-Agent') || '',
27
+ const userData: Record<string, string | string[]> = {
28
+ ...(email && { em: [await sha256(email) || ''] }),
29
+ ...(phoneNorm && { ph: [await sha256(phoneNorm) || ''] }),
30
+ ...(userId && { external_id: [await sha256(String(userId)) || ''] }),
31
+ client_ip_address: request?.headers.get('CF-Connecting-IP') || '',
32
+ client_user_agent: request?.headers.get('User-Agent') || '',
31
33
  };
32
34
 
33
35
  const body = {
@@ -40,8 +42,8 @@ export async function sendPinterestCapi(env, eventName, payload, request, ctx) {
40
42
  user_data: userData,
41
43
  custom_data: {
42
44
  currency: (currency || 'BRL').toUpperCase(),
43
- value: value ? String(parseFloat(value)) : '0',
44
- ...(contentIds?.length > 0 && { content_ids: contentIds.map(String) }),
45
+ value: value ? String(parseFloat(String(value))) : '0',
46
+ ...(contentIds && contentIds.length > 0 && { content_ids: contentIds.map(String) }),
45
47
  ...(contentName && { content_name: contentName }),
46
48
  content_type: 'product',
47
49
  },
@@ -55,20 +57,20 @@ export async function sendPinterestCapi(env, eventName, payload, request, ctx) {
55
57
  );
56
58
  const data = await res.json();
57
59
  if (!res.ok) {
58
- const msg = data.message || data.code || String(res.status);
60
+ const msg = (data as any).message || (data as any).code || String(res.status);
59
61
  console.error('Pinterest CAPI error:', res.status, msg);
60
- if (env.DB && ctx) ctx.waitUntil(logApiFailure(env.DB, 'pinterest', eventName, String(res.status), msg, body.data[0].event_id, JSON.stringify(body)));
62
+ if (env.DB && ctx) ctx.waitUntil(logApiFailure(env.DB, 'pinterest', eventName, String(res.status), msg, (body.data as any)[0].event_id, JSON.stringify(body)));
61
63
  }
62
64
  return data;
63
- } catch (err) {
64
- console.error('Pinterest CAPI fetch failed:', err.message);
65
- if (env.DB && ctx) ctx.waitUntil(logApiFailure(env.DB, 'pinterest', eventName, 'FETCH_ERROR', err.message, null, JSON.stringify(body)));
66
- return { error: err.message };
65
+ } catch (err: any) {
66
+ console.error('Pinterest CAPI fetch failed:', err?.message || String(err));
67
+ if (env.DB && ctx) ctx.waitUntil(logApiFailure(env.DB, 'pinterest', eventName, 'FETCH_ERROR', err?.message || String(err), '', JSON.stringify(body)));
68
+ return { error: err?.message || String(err) };
67
69
  }
68
70
  }
69
71
 
70
72
  // ── Reddit Conversions API v2.0 ───────────────────────────────────────────────
71
- export async function sendRedditCapi(env, eventName, payload, request, ctx) {
73
+ export async function sendRedditCapi(env: Env, eventName: string, payload: TrackPayload, request: Request | null, ctx: ExecutionContext | null): Promise<any> {
72
74
  if (!env.REDDIT_ACCESS_TOKEN || !env.REDDIT_AD_ACCOUNT_ID) {
73
75
  return { skipped: 'Reddit credentials not set' };
74
76
  }
@@ -76,25 +78,25 @@ export async function sendRedditCapi(env, eventName, payload, request, ctx) {
76
78
  const { email, phone, userId, eventId, pageUrl, value, currency } = payload;
77
79
  const phoneNorm = normalizePhone(phone);
78
80
 
79
- const redditEventMap = {
81
+ const redditEventMap: Record<string, string> = {
80
82
  PageView: 'PageVisit', ViewContent: 'ViewContent', Lead: 'Lead', Purchase: 'Purchase',
81
83
  AddToCart: 'AddToCart', InitiateCheckout: 'Purchase', CompleteRegistration: 'SignUp',
82
84
  Search: 'Search', Contact: 'Lead',
83
85
  };
84
86
  const rEvent = redditEventMap[eventName] || 'Custom';
85
87
 
86
- const user = {
87
- ...(email && { email: { value: await sha256(email) } }),
88
- ...(phoneNorm && { phoneNumber: { value: await sha256(phoneNorm) } }),
89
- ...(userId && { externalId: { value: await sha256(String(userId)) } }),
90
- ipAddress: { value: request.headers.get('CF-Connecting-IP') || '' },
91
- userAgent: { value: request.headers.get('User-Agent') || '' },
88
+ const user: Record<string, { value: string }> = {
89
+ ...(email && { email: { value: await sha256(email) || '' } }),
90
+ ...(phoneNorm && { phoneNumber: { value: await sha256(phoneNorm) || '' } }),
91
+ ...(userId && { externalId: { value: await sha256(String(userId)) || '' } }),
92
+ ipAddress: { value: request?.headers.get('CF-Connecting-IP') || '' },
93
+ userAgent: { value: request?.headers.get('User-Agent') || '' },
92
94
  };
93
95
 
94
- const event = {
96
+ const event: Record<string, any> = {
95
97
  event_at: new Date().toISOString(),
96
98
  event_type: { tracking_type: rEvent, ...(eventName === 'InitiateCheckout' && { conversion_type: 'BEGIN_CHECKOUT' }) },
97
- click_id: payload.rdtClid || '',
99
+ click_id: (payload as any).rdtClid || '',
98
100
  event_metadata: {
99
101
  currency: (currency || 'BRL').toUpperCase(),
100
102
  value_decimal: String(value || 0),
@@ -118,15 +120,15 @@ export async function sendRedditCapi(env, eventName, payload, request, ctx) {
118
120
  return { error: `HTTP ${res.status}` };
119
121
  }
120
122
  return await res.json();
121
- } catch (err) {
122
- console.error('Reddit CAPI fetch failed:', err.message);
123
- if (env.DB && ctx) ctx.waitUntil(logApiFailure(env.DB, 'reddit', eventName, 'FETCH_ERROR', err.message, null, JSON.stringify(body)));
124
- return { error: err.message };
123
+ } catch (err: any) {
124
+ console.error('Reddit CAPI fetch failed:', err?.message || String(err));
125
+ if (env.DB && ctx) ctx.waitUntil(logApiFailure(env.DB, 'reddit', eventName, 'FETCH_ERROR', err?.message || String(err), '', JSON.stringify(body)));
126
+ return { error: err?.message || String(err) };
125
127
  }
126
128
  }
127
129
 
128
130
  // ── LinkedIn Conversions API (LinkedIn-Version: 202401) ───────────────────────
129
- export async function sendLinkedInCapi(env, eventName, payload, request, ctx) {
131
+ export async function sendLinkedInCapi(env: Env, eventName: string, payload: TrackPayload, request: Request | null, ctx: ExecutionContext | null): Promise<any> {
130
132
  if (!env.LINKEDIN_ACCESS_TOKEN || !env.LINKEDIN_CONVERSION_ID) {
131
133
  return { skipped: 'LinkedIn credentials not set' };
132
134
  }
@@ -134,23 +136,23 @@ export async function sendLinkedInCapi(env, eventName, payload, request, ctx) {
134
136
  const { email, phone, firstName, lastName, userId, eventId, pageUrl, value, currency } = payload;
135
137
  const phoneNorm = normalizePhone(phone);
136
138
 
137
- const linkedInEventMap = {
139
+ const linkedInEventMap: Record<string, string> = {
138
140
  Lead: 'LEAD', Purchase: 'PURCHASE', CompleteRegistration: 'REGISTRATION',
139
141
  AddToCart: 'ADD_TO_CART', InitiateCheckout: 'OTHER', ViewContent: 'OTHER',
140
142
  PageView: 'OTHER', Contact: 'LEAD',
141
143
  };
142
144
 
143
- const userInfo = {
144
- ...(email && { 'SHA256_EMAIL': await sha256(email) }),
145
- ...(phoneNorm && { 'SHA256_PHONE': await sha256(phoneNorm) }),
146
- ...(firstName && { 'SHA256_FIRST_NAME': await sha256(firstName.toLowerCase().trim()) }),
147
- ...(lastName && { 'SHA256_LAST_NAME': await sha256(lastName.toLowerCase().trim()) }),
145
+ const userInfo: Record<string, string> = {
146
+ ...(email && { 'SHA256_EMAIL': await sha256(email) || '' }),
147
+ ...(phoneNorm && { 'SHA256_PHONE': await sha256(phoneNorm) || '' }),
148
+ ...(firstName && { 'SHA256_FIRST_NAME': await sha256(firstName.toLowerCase().trim()) || '' }),
149
+ ...(lastName && { 'SHA256_LAST_NAME': await sha256(lastName.toLowerCase().trim()) || '' }),
148
150
  };
149
151
 
150
- const body = {
152
+ const body: Record<string, any> = {
151
153
  conversion: `urn:li:conversion:${env.LINKEDIN_CONVERSION_ID}`,
152
154
  conversionHappenedAt: Date.now(),
153
- conversionValue: value ? { currencyCode: (currency || 'BRL').toUpperCase(), amount: String(parseFloat(value)) } : undefined,
155
+ conversionValue: value ? { currencyCode: (currency || 'BRL').toUpperCase(), amount: String(parseFloat(String(value))) } : undefined,
154
156
  eventId: eventId || `cdp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
155
157
  ...(Object.keys(userInfo).length > 0 && { user: { userIds: Object.entries(userInfo).map(([idType, idValue]) => ({ idType, idValue })) } }),
156
158
  };
@@ -173,15 +175,15 @@ export async function sendLinkedInCapi(env, eventName, payload, request, ctx) {
173
175
  return { error: `HTTP ${res.status}` };
174
176
  }
175
177
  return { ok: true };
176
- } catch (err) {
177
- console.error('LinkedIn CAPI fetch failed:', err.message);
178
- if (env.DB && ctx) ctx.waitUntil(logApiFailure(env.DB, 'linkedin', eventName, 'FETCH_ERROR', err.message, null, JSON.stringify(body)));
179
- return { error: err.message };
178
+ } catch (err: any) {
179
+ console.error('LinkedIn CAPI fetch failed:', err?.message || String(err));
180
+ if (env.DB && ctx) ctx.waitUntil(logApiFailure(env.DB, 'linkedin', eventName, 'FETCH_ERROR', err?.message || String(err), '', JSON.stringify(body)));
181
+ return { error: err?.message || String(err) };
180
182
  }
181
183
  }
182
184
 
183
185
  // ── Spotify Conversions API v1 ────────────────────────────────────────────────
184
- export async function sendSpotifyCapi(env, eventName, payload, request, ctx) {
186
+ export async function sendSpotifyCapi(env: Env, eventName: string, payload: TrackPayload, request: Request | null, ctx: ExecutionContext | null): Promise<any> {
185
187
  if (!env.SPOTIFY_ACCESS_TOKEN || !env.SPOTIFY_AD_ACCOUNT_ID) {
186
188
  return { skipped: 'Spotify credentials not set' };
187
189
  }
@@ -189,19 +191,19 @@ export async function sendSpotifyCapi(env, eventName, payload, request, ctx) {
189
191
  const { email, phone, userId, eventId, pageUrl, value, currency } = payload;
190
192
  const phoneNorm = normalizePhone(phone);
191
193
 
192
- const spotifyEventMap = {
194
+ const spotifyEventMap: Record<string, string> = {
193
195
  Purchase: 'PURCHASE', Lead: 'LEAD', CompleteRegistration: 'SIGN_UP',
194
196
  AddToCart: 'ADD_TO_CART', InitiateCheckout: 'INITIATE_CHECKOUT',
195
197
  ViewContent: 'VIEW_CONTENT', PageView: 'PAGE_VIEW', Contact: 'LEAD',
196
198
  };
197
199
  const spEvent = spotifyEventMap[eventName] || 'CUSTOM';
198
200
 
199
- const user = {
200
- ...(email && { hashed_email: await sha256(email) }),
201
- ...(phoneNorm && { hashed_phone: await sha256(phoneNorm) }),
201
+ const user: Record<string, string> = {
202
+ ...(email && { hashed_email: await sha256(email) || '' }),
203
+ ...(phoneNorm && { hashed_phone: await sha256(phoneNorm) || '' }),
202
204
  ...(userId && { user_id: userId }),
203
- ip_address: request.headers.get('CF-Connecting-IP') || '',
204
- user_agent: request.headers.get('User-Agent') || '',
205
+ ip_address: request?.headers.get('CF-Connecting-IP') || '',
206
+ user_agent: request?.headers.get('User-Agent') || '',
205
207
  };
206
208
 
207
209
  const body = {
@@ -212,7 +214,7 @@ export async function sendSpotifyCapi(env, eventName, payload, request, ctx) {
212
214
  url: pageUrl || '',
213
215
  user,
214
216
  ...(value !== undefined && {
215
- value: { currency: (currency || 'BRL').toUpperCase(), amount: parseFloat(value) },
217
+ value: { currency: (currency || 'BRL').toUpperCase(), amount: parseFloat(String(value)) },
216
218
  }),
217
219
  }],
218
220
  };
@@ -225,13 +227,13 @@ export async function sendSpotifyCapi(env, eventName, payload, request, ctx) {
225
227
  if (!res.ok) {
226
228
  const txt = await res.text();
227
229
  console.error('Spotify CAPI error:', txt);
228
- if (env.DB && ctx) ctx.waitUntil(logApiFailure(env.DB, 'spotify', eventName, String(res.status), txt, body.data[0].event_id, JSON.stringify(body)));
230
+ if (env.DB && ctx) ctx.waitUntil(logApiFailure(env.DB, 'spotify', eventName, String(res.status), txt, (body.data as any)[0].event_id, JSON.stringify(body)));
229
231
  return { error: `HTTP ${res.status}` };
230
232
  }
231
233
  return await res.json();
232
- } catch (err) {
233
- console.error('Spotify CAPI fetch failed:', err.message);
234
- if (env.DB && ctx) ctx.waitUntil(logApiFailure(env.DB, 'spotify', eventName, 'FETCH_ERROR', err.message, null, JSON.stringify(body)));
235
- return { error: err.message };
234
+ } catch (err: any) {
235
+ console.error('Spotify CAPI fetch failed:', err?.message || String(err));
236
+ if (env.DB && ctx) ctx.waitUntil(logApiFailure(env.DB, 'spotify', eventName, 'FETCH_ERROR', err?.message || String(err), '', JSON.stringify(body)));
237
+ return { error: err?.message || String(err) };
236
238
  }
237
239
  }
@@ -5,8 +5,10 @@
5
5
 
6
6
  import { sha256, normalizePhone } from '../utils.js';
7
7
  import { logApiFailure } from '../db.js';
8
+ import { Env, TrackPayload } from '../../types.js';
9
+ import { ExecutionContext } from '@cloudflare/workers-types';
8
10
 
9
- export async function sendTikTokApi(env, eventName, payload, request, ctx) {
11
+ export async function sendTikTokApi(env: Env, eventName: string, payload: TrackPayload, request: Request | null, ctx: ExecutionContext | null): Promise<any> {
10
12
  if (!env.TIKTOK_ACCESS_TOKEN) return { skipped: 'TIKTOK_ACCESS_TOKEN not set' };
11
13
 
12
14
  const pixelId = env.TIKTOK_PIXEL_ID;
@@ -22,41 +24,41 @@ export async function sendTikTokApi(env, eventName, payload, request, ctx) {
22
24
 
23
25
  const phoneNorm = normalizePhone(phone);
24
26
 
25
- const user = {
26
- ...(email && { email: await sha256(email) }),
27
- ...(phoneNorm && { phone_number: await sha256(phoneNorm) }),
28
- ...(userId && { external_id: await sha256(String(userId)) }),
27
+ const user: Record<string, string> = {
28
+ ...(email && { email: await sha256(email) || '' }),
29
+ ...(phoneNorm && { phone_number: await sha256(phoneNorm) || '' }),
30
+ ...(userId && { external_id: await sha256(String(userId)) || '' }),
29
31
  ...(ttp && { ttp }),
30
32
  ...(ttclid && { ttclid }),
31
33
  };
32
34
 
33
- const properties = {
34
- ...(value !== undefined && { value: parseFloat(value) }),
35
+ const properties: Record<string, any> = {
36
+ ...(value !== undefined && { value: parseFloat(String(value)) }),
35
37
  ...(currency && { currency: String(currency).toUpperCase() }),
36
- ...(contentIds && contentIds.length > 0 && {
38
+ ...(contentIds && contentIds && contentIds.length > 0 && {
37
39
  contents: contentIds.map(id => ({
38
40
  content_id: String(id),
39
41
  content_name: contentName || '',
40
42
  content_type: contentType || 'product',
41
43
  quantity: 1,
42
- price: value ? parseFloat(value) : 0,
44
+ price: value ? parseFloat(String(value)) : 0,
43
45
  })),
44
46
  }),
45
47
  };
46
48
 
47
- const event = {
49
+ const event: Record<string, any> = {
48
50
  event: eventName,
49
51
  event_time: Math.floor(Date.now() / 1000),
50
52
  event_id: eventId || `cdp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
51
53
  user,
52
54
  page: {
53
55
  url: pageUrl || `https://${env.SITE_DOMAIN}`,
54
- referrer: request.headers.get('Referer') || '',
56
+ referrer: request?.headers.get('Referer') || '',
55
57
  },
56
58
  ...(Object.keys(properties).length > 0 && { properties }),
57
59
  context: {
58
- ip: request.headers.get('CF-Connecting-IP') || '',
59
- user_agent: request.headers.get('User-Agent') || '',
60
+ ip: request?.headers.get('CF-Connecting-IP') || '',
61
+ user_agent: request?.headers.get('User-Agent') || '',
60
62
  },
61
63
  };
62
64
 
@@ -80,21 +82,21 @@ export async function sendTikTokApi(env, eventName, payload, request, ctx) {
80
82
  });
81
83
 
82
84
  const data = await res.json();
83
- if (!res.ok || data.code !== 0) {
84
- console.error('TikTok Events API error:', res.status, data.message || data.code || 'unknown');
85
+ if (!res.ok || (data as any).code !== 0) {
86
+ console.error('TikTok Events API error:', res.status, (data as any).message || (data as any).code || 'unknown');
85
87
 
86
88
  if (env.DB && ctx) {
87
- ctx.waitUntil(logApiFailure(env.DB, 'tiktok', eventName, String(data.code || res.status), data.message || 'TikTok API error', event.event_id, JSON.stringify(body)));
89
+ ctx.waitUntil(logApiFailure(env.DB, 'tiktok', eventName, String((data as any).code || res.status), (data as any).message || 'TikTok API error', event.event_id, JSON.stringify(body)));
88
90
  }
89
91
  }
90
92
  return data;
91
- } catch (err) {
92
- console.error('TikTok Events API fetch failed:', err.message);
93
+ } catch (err: any) {
94
+ console.error('TikTok Events API fetch failed:', err?.message || String(err));
93
95
 
94
96
  if (env.DB && ctx) {
95
- ctx.waitUntil(logApiFailure(env.DB, 'tiktok', eventName, 'FETCH_ERROR', err.message, null, JSON.stringify(body)));
97
+ ctx.waitUntil(logApiFailure(env.DB, 'tiktok', eventName, 'FETCH_ERROR', err?.message || String(err), '', JSON.stringify(body)));
96
98
  }
97
99
 
98
- return { error: err.message };
100
+ return { error: err?.message || String(err) };
99
101
  }
100
102
  }
@@ -5,9 +5,43 @@
5
5
 
6
6
  import { sha256, normalizePhone } from '../utils.js';
7
7
  import { saveLead, logApiFailure } from '../db.js';
8
+ import { Env, TrackPayload } from '../../types.js';
9
+ import { ExecutionContext } from '@cloudflare/workers-types';
10
+
11
+ // ── Tipos ───────────────────────────────────────────────────────────────────────
12
+ interface WhatsAppOptions {
13
+ to?: string;
14
+ template?: {
15
+ name: string;
16
+ language?: string;
17
+ components?: any[];
18
+ };
19
+ mediaType?: 'image' | 'document' | 'video' | 'audio';
20
+ mediaUrl?: string;
21
+ caption?: string;
22
+ filename?: string;
23
+ interactive?: 'buttons' | 'list';
24
+ bodyText?: string;
25
+ buttons?: Array<{ id: string; title: string }>;
26
+ listButton?: string;
27
+ rows?: Array<{ id: string; title: string; description?: string }>;
28
+ }
29
+
30
+ interface WhatsAppMessage {
31
+ from: string;
32
+ id: string;
33
+ type: string;
34
+ text?: { body: string };
35
+ referral?: {
36
+ ctwa_clid?: string;
37
+ source_id?: string;
38
+ source_url?: string;
39
+ headline?: string;
40
+ };
41
+ }
8
42
 
9
43
  // ── sendWhatsApp — envia mensagem via Meta Cloud API ──────────────────────────
10
- export async function sendWhatsApp(env, tipo, payload, options = {}) {
44
+ export async function sendWhatsApp(env: Env, tipo: string, payload: TrackPayload, options: WhatsAppOptions = {}): Promise<any> {
11
45
  if (!env.WHATSAPP_PHONE_NUMBER_ID || !env.WHATSAPP_ACCESS_TOKEN || !env.WA_NOTIFY_NUMBER) {
12
46
  return { skipped: 'WhatsApp não configurado' };
13
47
  }
@@ -21,14 +55,14 @@ export async function sendWhatsApp(env, tipo, payload, options = {}) {
21
55
  }
22
56
 
23
57
  if (options.mediaType && options.mediaUrl) {
24
- const mediaPayload = { link: options.mediaUrl };
58
+ const mediaPayload: Record<string, string> = { link: options.mediaUrl };
25
59
  if (options.caption) mediaPayload.caption = options.caption;
26
60
  if (options.filename) mediaPayload.filename = options.filename;
27
61
  const body = { messaging_product: 'whatsapp', to, type: options.mediaType, [options.mediaType]: mediaPayload };
28
62
  return _sendWARequest(env, body);
29
63
  }
30
64
 
31
- if (options.interactive === 'buttons' && options.buttons?.length) {
65
+ if (options.interactive === 'buttons' && options.buttons && options.buttons.length > 0) {
32
66
  const body = {
33
67
  messaging_product: 'whatsapp', to, type: 'interactive',
34
68
  interactive: {
@@ -42,7 +76,7 @@ export async function sendWhatsApp(env, tipo, payload, options = {}) {
42
76
  return _sendWARequest(env, body);
43
77
  }
44
78
 
45
- if (options.interactive === 'list' && options.rows?.length) {
79
+ if (options.interactive === 'list' && options.rows && options.rows.length > 0) {
46
80
  const body = {
47
81
  messaging_product: 'whatsapp', to, type: 'interactive',
48
82
  interactive: {
@@ -56,7 +90,7 @@ export async function sendWhatsApp(env, tipo, payload, options = {}) {
56
90
 
57
91
  // Text fallback (dentro da janela de 24h)
58
92
  const nome = [payload.firstName, payload.lastName].filter(Boolean).join(' ') || 'sem nome';
59
- const valor = payload.value ? `R$ ${parseFloat(payload.value).toFixed(2)}` : '—';
93
+ const valor = payload.value ? `R$ ${parseFloat(String(payload.value)).toFixed(2)}` : '—';
60
94
  const utm = payload.utmSource || 'direto';
61
95
  const produto = payload.contentName || '';
62
96
 
@@ -80,7 +114,7 @@ export async function sendWhatsApp(env, tipo, payload, options = {}) {
80
114
  }
81
115
 
82
116
  // ── _sendWARequest — executor interno ─────────────────────────────────────────
83
- async function _sendWARequest(env, body) {
117
+ async function _sendWARequest(env: Env, body: Record<string, any>): Promise<any> {
84
118
  try {
85
119
  const res = await fetch(`https://graph.facebook.com/v22.0/${env.WHATSAPP_PHONE_NUMBER_ID}/messages`, {
86
120
  method: 'POST',
@@ -88,16 +122,16 @@ async function _sendWARequest(env, body) {
88
122
  body: JSON.stringify(body),
89
123
  });
90
124
  const data = await res.json();
91
- if (!res.ok) console.error('WhatsApp Meta API error:', res.status, data.error?.message || 'unknown');
125
+ if (!res.ok) console.error('WhatsApp Meta API error:', res.status, (data as any).error?.message || 'unknown');
92
126
  return { ok: res.ok, status: res.status, data };
93
- } catch (err) {
94
- console.error('WhatsApp Meta API failed:', err.message);
95
- return { ok: false, error: err.message };
127
+ } catch (err: any) {
128
+ console.error('WhatsApp Meta API failed:', err?.message || String(err));
129
+ return { ok: false, error: err?.message || String(err) };
96
130
  }
97
131
  }
98
132
 
99
133
  // ── sendCallMeBot — alertas de sistema via WhatsApp ───────────────────────────
100
- export async function sendCallMeBot(env, mensagem) {
134
+ export async function sendCallMeBot(env: Env, mensagem: string): Promise<any> {
101
135
  if (!env.CALLMEBOT_PHONE || !env.CALLMEBOT_APIKEY) {
102
136
  return { skipped: 'CallMeBot não configurado' };
103
137
  }
@@ -105,22 +139,22 @@ export async function sendCallMeBot(env, mensagem) {
105
139
  const url = `https://api.callmebot.com/whatsapp.php?phone=${encodeURIComponent(env.CALLMEBOT_PHONE)}&text=${encodeURIComponent(mensagem)}&apikey=${env.CALLMEBOT_APIKEY}`;
106
140
  const res = await fetch(url);
107
141
  return { ok: res.ok, status: res.status };
108
- } catch (err) {
109
- console.error('CallMeBot failed:', err.message);
110
- return { ok: false, error: err.message };
142
+ } catch (err: any) {
143
+ console.error('CallMeBot failed:', err?.message || String(err));
144
+ return { ok: false, error: err?.message || String(err) };
111
145
  }
112
146
  }
113
147
 
114
148
  // ── processWhatsAppWebhook — CTWA (Click to WhatsApp) ────────────────────────
115
- export async function processWhatsAppWebhook(env, body, request, ctx) {
116
- const entry = body?.entry?.[0];
117
- const change = entry?.changes?.find(c => c.field === 'messages');
149
+ export async function processWhatsAppWebhook(env: Env, body: any, request: Request, ctx: ExecutionContext): Promise<any> {
150
+ const entry: any = body?.entry?.[0];
151
+ const change = entry?.changes?.find((c: any) => c.field === 'messages');
118
152
  if (!change) return { skipped: 'no messages field' };
119
153
 
120
154
  const messages = change.value?.messages;
121
155
  if (!messages || messages.length === 0) return { skipped: 'no messages' };
122
156
 
123
- const results = [];
157
+ const results: any[] = [];
124
158
 
125
159
  for (const message of messages) {
126
160
  const phone = message.from;
@@ -155,7 +189,7 @@ export async function processWhatsAppWebhook(env, body, request, ctx) {
155
189
  );
156
190
  }
157
191
 
158
- const capiEvent = {
192
+ const capiEvent: Record<string, any> = {
159
193
  event_name: 'Contact',
160
194
  event_time: Math.floor(Date.now() / 1000),
161
195
  event_id: eventId,
@@ -172,7 +206,7 @@ export async function processWhatsAppWebhook(env, body, request, ctx) {
172
206
  ctx.waitUntil(
173
207
  (async () => {
174
208
  try {
175
- const requestBody = { data: [capiEvent], access_token: env.META_ACCESS_TOKEN };
209
+ const requestBody: Record<string, any> = { data: [capiEvent], access_token: env.META_ACCESS_TOKEN };
176
210
  if (env.META_TEST_CODE) requestBody.test_event_code = env.META_TEST_CODE;
177
211
 
178
212
  const res = await fetch(
@@ -184,13 +218,13 @@ export async function processWhatsAppWebhook(env, body, request, ctx) {
184
218
  if (res.ok && env.DB && wamid) {
185
219
  await env.DB.prepare('UPDATE whatsapp_contacts SET capi_sent = 1 WHERE wamid = ?').bind(wamid).run();
186
220
  } else if (!res.ok) {
187
- console.error('[CTWA] Meta CAPI error:', res.status, data.error?.message || 'unknown');
221
+ console.error('[CTWA] Meta CAPI error:', res.status, (data as any).error?.message || 'unknown');
188
222
  if (env.DB) {
189
- await logApiFailure(env.DB, 'meta', 'Contact', data.error?.code || res.status, data.error?.message || 'CTWA CAPI error', eventId, JSON.stringify(requestBody));
223
+ await logApiFailure(env.DB, 'meta', 'Contact', (data as any).error?.code || res.status, (data as any).error?.message || 'CTWA CAPI error', eventId, JSON.stringify(requestBody));
190
224
  }
191
225
  }
192
- } catch (err) {
193
- console.error('[CTWA] Meta CAPI fetch failed:', err.message);
226
+ } catch (err: any) {
227
+ console.error('[CTWA] Meta CAPI fetch failed:', err?.message || String(err));
194
228
  }
195
229
  })()
196
230
  );
@@ -209,7 +243,7 @@ export async function processWhatsAppWebhook(env, body, request, ctx) {
209
243
  }
210
244
 
211
245
  // ── verifyHmac — validação constant-time de assinatura HMAC-SHA256 ─────────────
212
- export async function verifyHmac(secret, rawBody, receivedSignature) {
246
+ export async function verifyHmac(secret: string, rawBody: string, receivedSignature: string): Promise<boolean> {
213
247
  if (!secret || !receivedSignature) return false;
214
248
  try {
215
249
  const key = await crypto.subtle.importKey(