cdp-edge 1.23.3 → 1.24.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 +44 -2
- package/bin/cdp-edge.js +10 -1
- package/contracts/types.ts +81 -0
- package/dist/commands/install.js +6 -1
- package/docs/whatsapp-ctwa.md +3 -2
- package/package.json +7 -4
- package/server-edge-tracker/{index.js → index.ts} +91 -82
- package/server-edge-tracker/modules/{db.js → db.ts} +116 -76
- 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} +59 -25
- 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 +251 -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,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
|
|
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
|
|
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
|
|
95
|
-
return { ok: false, error: err
|
|
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
|
|
110
|
-
return { ok: false, error: err
|
|
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
|
|
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
|
|
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(
|