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.
- package/README.md +57 -2
- package/contracts/types.ts +81 -0
- package/docs/whatsapp-ctwa.md +3 -2
- package/package.json +7 -4
- package/server-edge-tracker/.client.env.example +14 -0
- package/server-edge-tracker/deploy-client.js +76 -0
- package/server-edge-tracker/{index.js → index.ts} +93 -84
- package/server-edge-tracker/modules/{db.js → db.ts} +117 -77
- package/server-edge-tracker/modules/dispatch/{ga4.js → ga4.ts} +12 -10
- package/server-edge-tracker/modules/dispatch/{meta.js → meta.ts} +35 -28
- package/server-edge-tracker/modules/dispatch/{platforms.js → platforms.ts} +58 -56
- package/server-edge-tracker/modules/dispatch/{tiktok.js → tiktok.ts} +22 -20
- package/server-edge-tracker/modules/dispatch/{whatsapp.js → whatsapp.ts} +74 -28
- package/server-edge-tracker/modules/{intelligence.js → intelligence.ts} +175 -60
- package/server-edge-tracker/modules/ml/{bidding.js → bidding.ts} +37 -35
- package/server-edge-tracker/modules/ml/{fraud.js → fraud.ts} +48 -40
- package/server-edge-tracker/modules/ml/{logistic.js → logistic.ts} +44 -19
- package/server-edge-tracker/modules/ml/{ltv.js → ltv.ts} +135 -90
- package/server-edge-tracker/modules/ml/{matchquality.js → matchquality.ts} +70 -26
- package/server-edge-tracker/modules/ml/{segmentation.js → segmentation.ts} +109 -48
- package/server-edge-tracker/modules/{utils.js → utils.ts} +41 -22
- package/server-edge-tracker/types.ts +255 -0
- package/server-edge-tracker/wrangler.toml +2 -2
- 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
|
|
30
|
-
client_user_agent: request
|
|
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
|
|
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
|
|
65
|
-
if (env.DB && ctx) ctx.waitUntil(logApiFailure(env.DB, 'pinterest', eventName, 'FETCH_ERROR', err
|
|
66
|
-
return { error: err
|
|
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
|
|
91
|
-
userAgent: { value: request
|
|
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
|
|
123
|
-
if (env.DB && ctx) ctx.waitUntil(logApiFailure(env.DB, 'reddit', eventName, 'FETCH_ERROR', err
|
|
124
|
-
return { error: err
|
|
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
|
|
178
|
-
if (env.DB && ctx) ctx.waitUntil(logApiFailure(env.DB, 'linkedin', eventName, 'FETCH_ERROR', err
|
|
179
|
-
return { error: err
|
|
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
|
|
204
|
-
user_agent: request
|
|
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
|
|
234
|
-
if (env.DB && ctx) ctx.waitUntil(logApiFailure(env.DB, 'spotify', eventName, 'FETCH_ERROR', err
|
|
235
|
-
return { error: err
|
|
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
|
|
56
|
+
referrer: request?.headers.get('Referer') || '',
|
|
55
57
|
},
|
|
56
58
|
...(Object.keys(properties).length > 0 && { properties }),
|
|
57
59
|
context: {
|
|
58
|
-
ip: request
|
|
59
|
-
user_agent: request
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 ${
|
|
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
|
|
95
|
-
return { ok: false, error: err
|
|
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
|
|
110
|
-
return { ok: false, error: err
|
|
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
|
|
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
|
|
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(
|