cdp-edge 2.2.5 → 2.3.1

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 +57 -2
  2. package/contracts/types.ts +81 -0
  3. package/docs/whatsapp-ctwa.md +3 -2
  4. package/package.json +7 -4
  5. package/server-edge-tracker/.client.env.example +14 -0
  6. package/server-edge-tracker/deploy-client.js +76 -0
  7. package/server-edge-tracker/{index.js → index.ts} +93 -84
  8. package/server-edge-tracker/modules/{db.js → db.ts} +117 -77
  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} +74 -28
  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 +255 -0
  23. package/server-edge-tracker/wrangler.toml +2 -2
  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,10 +5,54 @@
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
+ }
42
+
43
+ // ── Resolvedores de secrets (canônico + legado) ────────────────────────────────
44
+ // Meta Cloud API v22.0 usa PHONE_NUMBER_ID e ACCESS_TOKEN como termos oficiais.
45
+ // Suportamos ambos os nomes para compatibilidade com secrets já configurados.
46
+ function resolvePhoneNumberId(env: Env): string | undefined {
47
+ return env.WHATSAPP_PHONE_NUMBER_ID || env.WA_PHONE_ID;
48
+ }
49
+ function resolveAccessToken(env: Env): string | undefined {
50
+ return env.WHATSAPP_ACCESS_TOKEN || env.WA_ACCESS_TOKEN;
51
+ }
8
52
 
9
53
  // ── sendWhatsApp — envia mensagem via Meta Cloud API ──────────────────────────
10
- export async function sendWhatsApp(env, tipo, payload, options = {}) {
11
- if (!env.WHATSAPP_PHONE_NUMBER_ID || !env.WHATSAPP_ACCESS_TOKEN || !env.WA_NOTIFY_NUMBER) {
54
+ export async function sendWhatsApp(env: Env, tipo: string, payload: TrackPayload, options: WhatsAppOptions = {}): Promise<any> {
55
+ if (!resolvePhoneNumberId(env) || !resolveAccessToken(env) || !env.WA_NOTIFY_NUMBER) {
12
56
  return { skipped: 'WhatsApp não configurado' };
13
57
  }
14
58
 
@@ -21,14 +65,14 @@ export async function sendWhatsApp(env, tipo, payload, options = {}) {
21
65
  }
22
66
 
23
67
  if (options.mediaType && options.mediaUrl) {
24
- const mediaPayload = { link: options.mediaUrl };
68
+ const mediaPayload: Record<string, string> = { link: options.mediaUrl };
25
69
  if (options.caption) mediaPayload.caption = options.caption;
26
70
  if (options.filename) mediaPayload.filename = options.filename;
27
71
  const body = { messaging_product: 'whatsapp', to, type: options.mediaType, [options.mediaType]: mediaPayload };
28
72
  return _sendWARequest(env, body);
29
73
  }
30
74
 
31
- if (options.interactive === 'buttons' && options.buttons?.length) {
75
+ if (options.interactive === 'buttons' && options.buttons && options.buttons.length > 0) {
32
76
  const body = {
33
77
  messaging_product: 'whatsapp', to, type: 'interactive',
34
78
  interactive: {
@@ -42,7 +86,7 @@ export async function sendWhatsApp(env, tipo, payload, options = {}) {
42
86
  return _sendWARequest(env, body);
43
87
  }
44
88
 
45
- if (options.interactive === 'list' && options.rows?.length) {
89
+ if (options.interactive === 'list' && options.rows && options.rows.length > 0) {
46
90
  const body = {
47
91
  messaging_product: 'whatsapp', to, type: 'interactive',
48
92
  interactive: {
@@ -56,7 +100,7 @@ export async function sendWhatsApp(env, tipo, payload, options = {}) {
56
100
 
57
101
  // Text fallback (dentro da janela de 24h)
58
102
  const nome = [payload.firstName, payload.lastName].filter(Boolean).join(' ') || 'sem nome';
59
- const valor = payload.value ? `R$ ${parseFloat(payload.value).toFixed(2)}` : '—';
103
+ const valor = payload.value ? `R$ ${parseFloat(String(payload.value)).toFixed(2)}` : '—';
60
104
  const utm = payload.utmSource || 'direto';
61
105
  const produto = payload.contentName || '';
62
106
 
@@ -80,24 +124,26 @@ export async function sendWhatsApp(env, tipo, payload, options = {}) {
80
124
  }
81
125
 
82
126
  // ── _sendWARequest — executor interno ─────────────────────────────────────────
83
- async function _sendWARequest(env, body) {
127
+ async function _sendWARequest(env: Env, body: Record<string, any>): Promise<any> {
84
128
  try {
85
- const res = await fetch(`https://graph.facebook.com/v22.0/${env.WHATSAPP_PHONE_NUMBER_ID}/messages`, {
129
+ const phoneNumberId = resolvePhoneNumberId(env);
130
+ const accessToken = resolveAccessToken(env);
131
+ const res = await fetch(`https://graph.facebook.com/v22.0/${phoneNumberId}/messages`, {
86
132
  method: 'POST',
87
- headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${env.WHATSAPP_ACCESS_TOKEN}` },
133
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}` },
88
134
  body: JSON.stringify(body),
89
135
  });
90
136
  const data = await res.json();
91
- if (!res.ok) console.error('WhatsApp Meta API error:', res.status, data.error?.message || 'unknown');
137
+ if (!res.ok) console.error('WhatsApp Meta API error:', res.status, (data as any).error?.message || 'unknown');
92
138
  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 };
139
+ } catch (err: any) {
140
+ console.error('WhatsApp Meta API failed:', err?.message || String(err));
141
+ return { ok: false, error: err?.message || String(err) };
96
142
  }
97
143
  }
98
144
 
99
145
  // ── sendCallMeBot — alertas de sistema via WhatsApp ───────────────────────────
100
- export async function sendCallMeBot(env, mensagem) {
146
+ export async function sendCallMeBot(env: Env, mensagem: string): Promise<any> {
101
147
  if (!env.CALLMEBOT_PHONE || !env.CALLMEBOT_APIKEY) {
102
148
  return { skipped: 'CallMeBot não configurado' };
103
149
  }
@@ -105,22 +151,22 @@ export async function sendCallMeBot(env, mensagem) {
105
151
  const url = `https://api.callmebot.com/whatsapp.php?phone=${encodeURIComponent(env.CALLMEBOT_PHONE)}&text=${encodeURIComponent(mensagem)}&apikey=${env.CALLMEBOT_APIKEY}`;
106
152
  const res = await fetch(url);
107
153
  return { ok: res.ok, status: res.status };
108
- } catch (err) {
109
- console.error('CallMeBot failed:', err.message);
110
- return { ok: false, error: err.message };
154
+ } catch (err: any) {
155
+ console.error('CallMeBot failed:', err?.message || String(err));
156
+ return { ok: false, error: err?.message || String(err) };
111
157
  }
112
158
  }
113
159
 
114
160
  // ── 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');
161
+ export async function processWhatsAppWebhook(env: Env, body: any, request: Request, ctx: ExecutionContext): Promise<any> {
162
+ const entry: any = body?.entry?.[0];
163
+ const change = entry?.changes?.find((c: any) => c.field === 'messages');
118
164
  if (!change) return { skipped: 'no messages field' };
119
165
 
120
166
  const messages = change.value?.messages;
121
167
  if (!messages || messages.length === 0) return { skipped: 'no messages' };
122
168
 
123
- const results = [];
169
+ const results: any[] = [];
124
170
 
125
171
  for (const message of messages) {
126
172
  const phone = message.from;
@@ -155,7 +201,7 @@ export async function processWhatsAppWebhook(env, body, request, ctx) {
155
201
  );
156
202
  }
157
203
 
158
- const capiEvent = {
204
+ const capiEvent: Record<string, any> = {
159
205
  event_name: 'Contact',
160
206
  event_time: Math.floor(Date.now() / 1000),
161
207
  event_id: eventId,
@@ -172,7 +218,7 @@ export async function processWhatsAppWebhook(env, body, request, ctx) {
172
218
  ctx.waitUntil(
173
219
  (async () => {
174
220
  try {
175
- const requestBody = { data: [capiEvent], access_token: env.META_ACCESS_TOKEN };
221
+ const requestBody: Record<string, any> = { data: [capiEvent], access_token: env.META_ACCESS_TOKEN };
176
222
  if (env.META_TEST_CODE) requestBody.test_event_code = env.META_TEST_CODE;
177
223
 
178
224
  const res = await fetch(
@@ -184,13 +230,13 @@ export async function processWhatsAppWebhook(env, body, request, ctx) {
184
230
  if (res.ok && env.DB && wamid) {
185
231
  await env.DB.prepare('UPDATE whatsapp_contacts SET capi_sent = 1 WHERE wamid = ?').bind(wamid).run();
186
232
  } else if (!res.ok) {
187
- console.error('[CTWA] Meta CAPI error:', res.status, data.error?.message || 'unknown');
233
+ console.error('[CTWA] Meta CAPI error:', res.status, (data as any).error?.message || 'unknown');
188
234
  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));
235
+ 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
236
  }
191
237
  }
192
- } catch (err) {
193
- console.error('[CTWA] Meta CAPI fetch failed:', err.message);
238
+ } catch (err: any) {
239
+ console.error('[CTWA] Meta CAPI fetch failed:', err?.message || String(err));
194
240
  }
195
241
  })()
196
242
  );
@@ -209,7 +255,7 @@ export async function processWhatsAppWebhook(env, body, request, ctx) {
209
255
  }
210
256
 
211
257
  // ── verifyHmac — validação constant-time de assinatura HMAC-SHA256 ─────────────
212
- export async function verifyHmac(secret, rawBody, receivedSignature) {
258
+ export async function verifyHmac(secret: string, rawBody: string, receivedSignature: string): Promise<boolean> {
213
259
  if (!secret || !receivedSignature) return false;
214
260
  try {
215
261
  const key = await crypto.subtle.importKey(