better-zap 0.2.0 → 0.2.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/dist/client-DNlBPLBd.d.cts +850 -0
- package/dist/client-DiHHMG8t.d.mts +850 -0
- package/dist/client.cjs +97 -0
- package/dist/client.d.cts +2 -0
- package/dist/client.d.mts +2 -0
- package/dist/client.mjs +93 -0
- package/dist/index.cjs +2 -0
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/package.json +24 -10
|
@@ -0,0 +1,850 @@
|
|
|
1
|
+
//#region src/types/whatsapp.types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Meta WhatsApp Cloud API v25.0 Type Definitions
|
|
4
|
+
* https://developers.facebook.com/docs/whatsapp/cloud-api/reference/messages
|
|
5
|
+
*/
|
|
6
|
+
interface WhatsAppTextMessage {
|
|
7
|
+
messaging_product: "whatsapp";
|
|
8
|
+
recipient_type: "individual";
|
|
9
|
+
to: string;
|
|
10
|
+
type: "text";
|
|
11
|
+
text: {
|
|
12
|
+
preview_url?: boolean;
|
|
13
|
+
body: string;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
interface WhatsAppTemplateMessage {
|
|
17
|
+
messaging_product: "whatsapp";
|
|
18
|
+
to: string;
|
|
19
|
+
type: "template";
|
|
20
|
+
template: {
|
|
21
|
+
name: string;
|
|
22
|
+
language: {
|
|
23
|
+
code: string;
|
|
24
|
+
};
|
|
25
|
+
components?: TemplateComponent[];
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
interface WhatsAppInteractiveButtonsMessage {
|
|
29
|
+
messaging_product: "whatsapp";
|
|
30
|
+
recipient_type: "individual";
|
|
31
|
+
to: string;
|
|
32
|
+
type: "interactive";
|
|
33
|
+
interactive: {
|
|
34
|
+
type: "button";
|
|
35
|
+
body: {
|
|
36
|
+
text: string;
|
|
37
|
+
};
|
|
38
|
+
action: {
|
|
39
|
+
buttons: Array<{
|
|
40
|
+
type: "reply";
|
|
41
|
+
reply: {
|
|
42
|
+
id: string;
|
|
43
|
+
title: string;
|
|
44
|
+
};
|
|
45
|
+
}>;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
interface WhatsAppInteractiveListMessage {
|
|
50
|
+
messaging_product: "whatsapp";
|
|
51
|
+
recipient_type: "individual";
|
|
52
|
+
to: string;
|
|
53
|
+
type: "interactive";
|
|
54
|
+
interactive: {
|
|
55
|
+
type: "list";
|
|
56
|
+
body: {
|
|
57
|
+
text: string;
|
|
58
|
+
};
|
|
59
|
+
action: {
|
|
60
|
+
button: string;
|
|
61
|
+
sections: Array<{
|
|
62
|
+
title: string;
|
|
63
|
+
rows: Array<{
|
|
64
|
+
id: string;
|
|
65
|
+
title: string;
|
|
66
|
+
description?: string;
|
|
67
|
+
}>;
|
|
68
|
+
}>;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
interface WhatsAppInteractiveMediaCarouselMessage {
|
|
73
|
+
messaging_product: "whatsapp";
|
|
74
|
+
recipient_type: "individual";
|
|
75
|
+
to: string;
|
|
76
|
+
type: "interactive";
|
|
77
|
+
interactive: {
|
|
78
|
+
type: "carousel";
|
|
79
|
+
body: {
|
|
80
|
+
text: string;
|
|
81
|
+
};
|
|
82
|
+
action: {
|
|
83
|
+
cards: WhatsAppCarouselCard[];
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
interface InteractiveMediaCarouselCardInput {
|
|
88
|
+
header: {
|
|
89
|
+
type: "image" | "video";
|
|
90
|
+
link: string;
|
|
91
|
+
};
|
|
92
|
+
bodyText?: string;
|
|
93
|
+
button: {
|
|
94
|
+
displayText: string;
|
|
95
|
+
url: string;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
interface SendInteractiveMediaCarouselData {
|
|
99
|
+
to: string;
|
|
100
|
+
body: string;
|
|
101
|
+
cards: InteractiveMediaCarouselCardInput[];
|
|
102
|
+
}
|
|
103
|
+
interface WhatsAppCarouselCard {
|
|
104
|
+
card_index: number;
|
|
105
|
+
type: "cta_url";
|
|
106
|
+
header: {
|
|
107
|
+
type: "image" | "video";
|
|
108
|
+
image?: {
|
|
109
|
+
link: string;
|
|
110
|
+
};
|
|
111
|
+
video?: {
|
|
112
|
+
link: string;
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
body?: {
|
|
116
|
+
text: string;
|
|
117
|
+
};
|
|
118
|
+
action: {
|
|
119
|
+
name: "cta_url";
|
|
120
|
+
parameters: {
|
|
121
|
+
display_text: string;
|
|
122
|
+
url: string;
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
interface WhatsAppLocationMessage {
|
|
127
|
+
messaging_product: "whatsapp";
|
|
128
|
+
recipient_type: "individual";
|
|
129
|
+
to: string;
|
|
130
|
+
type: "location";
|
|
131
|
+
location: {
|
|
132
|
+
latitude: number;
|
|
133
|
+
longitude: number;
|
|
134
|
+
name: string;
|
|
135
|
+
address: string;
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
interface TemplateComponent {
|
|
139
|
+
type: "header" | "body" | "button";
|
|
140
|
+
sub_type?: "url" | "quick_reply";
|
|
141
|
+
index?: string;
|
|
142
|
+
parameters: TemplateParameter[];
|
|
143
|
+
}
|
|
144
|
+
interface TemplateParameter {
|
|
145
|
+
type: "text" | "currency" | "date_time" | "image" | "document" | "video" | "location" | "payload";
|
|
146
|
+
parameter_name?: string;
|
|
147
|
+
text?: string;
|
|
148
|
+
currency?: {
|
|
149
|
+
fallback_value: string;
|
|
150
|
+
code: string;
|
|
151
|
+
amount_1000: number;
|
|
152
|
+
};
|
|
153
|
+
date_time?: {
|
|
154
|
+
fallback_value: string;
|
|
155
|
+
};
|
|
156
|
+
image?: {
|
|
157
|
+
link: string;
|
|
158
|
+
};
|
|
159
|
+
video?: {
|
|
160
|
+
link: string;
|
|
161
|
+
};
|
|
162
|
+
document?: {
|
|
163
|
+
link: string;
|
|
164
|
+
};
|
|
165
|
+
location?: {
|
|
166
|
+
latitude: number;
|
|
167
|
+
longitude: number;
|
|
168
|
+
name: string;
|
|
169
|
+
address: string;
|
|
170
|
+
};
|
|
171
|
+
payload?: string;
|
|
172
|
+
}
|
|
173
|
+
interface SendMessageResponse {
|
|
174
|
+
messaging_product: "whatsapp";
|
|
175
|
+
contacts: Array<{
|
|
176
|
+
input: string;
|
|
177
|
+
wa_id: string;
|
|
178
|
+
}>;
|
|
179
|
+
messages: Array<{
|
|
180
|
+
id: string;
|
|
181
|
+
}>;
|
|
182
|
+
}
|
|
183
|
+
interface SendMessageError {
|
|
184
|
+
error: {
|
|
185
|
+
message: string;
|
|
186
|
+
type: string;
|
|
187
|
+
code: number;
|
|
188
|
+
error_subcode?: number;
|
|
189
|
+
error_data?: {
|
|
190
|
+
messaging_product: string;
|
|
191
|
+
details: string;
|
|
192
|
+
};
|
|
193
|
+
fbtrace_id: string;
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
interface WebhookPayload {
|
|
197
|
+
object: "whatsapp_business_account";
|
|
198
|
+
entry: WebhookEntry[];
|
|
199
|
+
}
|
|
200
|
+
interface WebhookEntry {
|
|
201
|
+
id: string;
|
|
202
|
+
changes: WebhookChange[];
|
|
203
|
+
}
|
|
204
|
+
interface WebhookChange {
|
|
205
|
+
value: WebhookValue;
|
|
206
|
+
field: WhatsAppWebhookField;
|
|
207
|
+
}
|
|
208
|
+
type WhatsAppWebhookField = "messages" | "message_template_status_update" | "message_template_quality_update" | "phone_number_name_update" | "phone_number_quality_update" | "account_update" | "security" | "history" | "smb_app_state_sync" | "smb_message_echoes" | "account_offboarded" | "account_reconnected" | (string & {});
|
|
209
|
+
interface WebhookValue {
|
|
210
|
+
messaging_product: "whatsapp";
|
|
211
|
+
metadata: {
|
|
212
|
+
display_phone_number: string;
|
|
213
|
+
phone_number_id: string;
|
|
214
|
+
};
|
|
215
|
+
contacts?: WebhookContact[];
|
|
216
|
+
messages?: IncomingMessage[];
|
|
217
|
+
statuses?: MessageStatus[];
|
|
218
|
+
errors?: WebhookError[];
|
|
219
|
+
}
|
|
220
|
+
interface WebhookContact {
|
|
221
|
+
profile: {
|
|
222
|
+
name: string;
|
|
223
|
+
};
|
|
224
|
+
wa_id: string;
|
|
225
|
+
}
|
|
226
|
+
interface IncomingMessage {
|
|
227
|
+
from: string;
|
|
228
|
+
id: string;
|
|
229
|
+
timestamp: string;
|
|
230
|
+
type: "text" | "image" | "audio" | "video" | "document" | "location" | "contacts" | "interactive" | "button" | "reaction" | "sticker" | "unsupported" | (string & {});
|
|
231
|
+
text?: {
|
|
232
|
+
body: string;
|
|
233
|
+
};
|
|
234
|
+
image?: MediaMessage;
|
|
235
|
+
audio?: MediaMessage;
|
|
236
|
+
video?: MediaMessage;
|
|
237
|
+
document?: MediaMessage & {
|
|
238
|
+
filename?: string;
|
|
239
|
+
};
|
|
240
|
+
location?: {
|
|
241
|
+
latitude: number;
|
|
242
|
+
longitude: number;
|
|
243
|
+
name?: string;
|
|
244
|
+
address?: string;
|
|
245
|
+
};
|
|
246
|
+
button?: {
|
|
247
|
+
text: string;
|
|
248
|
+
payload: string;
|
|
249
|
+
};
|
|
250
|
+
interactive?: {
|
|
251
|
+
type: "button_reply" | "list_reply";
|
|
252
|
+
button_reply?: {
|
|
253
|
+
id: string;
|
|
254
|
+
title: string;
|
|
255
|
+
};
|
|
256
|
+
list_reply?: {
|
|
257
|
+
id: string;
|
|
258
|
+
title: string;
|
|
259
|
+
description?: string;
|
|
260
|
+
};
|
|
261
|
+
};
|
|
262
|
+
context?: {
|
|
263
|
+
from: string;
|
|
264
|
+
id: string;
|
|
265
|
+
};
|
|
266
|
+
referral?: {
|
|
267
|
+
source_url: string;
|
|
268
|
+
source_type: "ad" | "post";
|
|
269
|
+
source_id: string;
|
|
270
|
+
headline: string;
|
|
271
|
+
body: string;
|
|
272
|
+
ctwa_clid?: string;
|
|
273
|
+
};
|
|
274
|
+
errors?: MessageError[];
|
|
275
|
+
edited?: boolean;
|
|
276
|
+
revoked?: boolean;
|
|
277
|
+
unsupported?: true;
|
|
278
|
+
[key: string]: unknown;
|
|
279
|
+
}
|
|
280
|
+
interface MediaMessage {
|
|
281
|
+
id: string;
|
|
282
|
+
mime_type: string;
|
|
283
|
+
sha256?: string;
|
|
284
|
+
caption?: string;
|
|
285
|
+
}
|
|
286
|
+
interface MessageStatus {
|
|
287
|
+
id: string;
|
|
288
|
+
status: "sent" | "delivered" | "read" | "failed";
|
|
289
|
+
timestamp: string;
|
|
290
|
+
recipient_id: string;
|
|
291
|
+
conversation?: {
|
|
292
|
+
id: string;
|
|
293
|
+
origin: {
|
|
294
|
+
type: "business_initiated" | "user_initiated" | "referral_conversion";
|
|
295
|
+
};
|
|
296
|
+
expiration_timestamp?: string;
|
|
297
|
+
};
|
|
298
|
+
pricing?: {
|
|
299
|
+
billable: boolean;
|
|
300
|
+
pricing_model: "CBP";
|
|
301
|
+
category: "business_initiated" | "user_initiated" | "referral_conversion";
|
|
302
|
+
};
|
|
303
|
+
errors?: MessageError[];
|
|
304
|
+
}
|
|
305
|
+
interface MessageError {
|
|
306
|
+
code: number;
|
|
307
|
+
title: string;
|
|
308
|
+
message: string;
|
|
309
|
+
error_data?: {
|
|
310
|
+
details: string;
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
interface WebhookError {
|
|
314
|
+
code: number;
|
|
315
|
+
title: string;
|
|
316
|
+
message: string;
|
|
317
|
+
error_data: {
|
|
318
|
+
details: string;
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
interface SendResult {
|
|
322
|
+
success: boolean;
|
|
323
|
+
messageId?: string;
|
|
324
|
+
error?: string;
|
|
325
|
+
errorCode?: number;
|
|
326
|
+
code?: string;
|
|
327
|
+
httpStatus?: number;
|
|
328
|
+
details?: Record<string, any>;
|
|
329
|
+
}
|
|
330
|
+
type FreeformMessageWindow = {
|
|
331
|
+
isOpen: boolean;
|
|
332
|
+
lastIncomingMessageAt: string | null;
|
|
333
|
+
expiresAt: string | null;
|
|
334
|
+
};
|
|
335
|
+
type ConversationRecord = {
|
|
336
|
+
id: string;
|
|
337
|
+
phone: string;
|
|
338
|
+
contactName: string | null;
|
|
339
|
+
unreadCount: number;
|
|
340
|
+
status: string;
|
|
341
|
+
lastMessageAt: string;
|
|
342
|
+
lastMessagePreview: string | null;
|
|
343
|
+
lastDirection: string;
|
|
344
|
+
messageCount: number;
|
|
345
|
+
lastIncomingMessageAt: string | null;
|
|
346
|
+
};
|
|
347
|
+
type Conversation = ConversationRecord & {
|
|
348
|
+
freeformMessageWindow: FreeformMessageWindow;
|
|
349
|
+
};
|
|
350
|
+
type UIMessageStatus = "sent" | "delivered" | "read" | "failed";
|
|
351
|
+
type UIMessage = {
|
|
352
|
+
id: string;
|
|
353
|
+
phone?: string;
|
|
354
|
+
content: string | null;
|
|
355
|
+
direction: "incoming" | "outgoing";
|
|
356
|
+
status: UIMessageStatus | string;
|
|
357
|
+
sentAt: string;
|
|
358
|
+
templateName?: string | null;
|
|
359
|
+
messageType?: string | null;
|
|
360
|
+
metadata?: Record<string, any> | null;
|
|
361
|
+
};
|
|
362
|
+
//#endregion
|
|
363
|
+
//#region src/types/config.d.ts
|
|
364
|
+
/** Configuration for {@link WhatsAppService}. */
|
|
365
|
+
interface WhatsAppConfig {
|
|
366
|
+
/** System User access token for the WhatsApp Cloud API. */
|
|
367
|
+
token: string;
|
|
368
|
+
/** Phone number ID registered in Meta Business Manager. */
|
|
369
|
+
phoneId: string;
|
|
370
|
+
/** Set to `"development"` to skip real API calls and return mock responses. */
|
|
371
|
+
environment?: string;
|
|
372
|
+
}
|
|
373
|
+
//#endregion
|
|
374
|
+
//#region src/types/sync-events.d.ts
|
|
375
|
+
type ConversationSummary = Conversation;
|
|
376
|
+
type NewMessageEvent = {
|
|
377
|
+
type: "NEW_MESSAGE";
|
|
378
|
+
message: WhatsAppLogRecord;
|
|
379
|
+
conversation: ConversationSummary;
|
|
380
|
+
};
|
|
381
|
+
type StatusUpdateEvent = {
|
|
382
|
+
type: "STATUS_UPDATE";
|
|
383
|
+
waMessageId: string;
|
|
384
|
+
status: WhatsAppStatus;
|
|
385
|
+
timestamp: string;
|
|
386
|
+
deliveredAt?: string | null;
|
|
387
|
+
readAt?: string | null;
|
|
388
|
+
};
|
|
389
|
+
type ConversationUpdateEvent = {
|
|
390
|
+
type: "CONVERSATION_UPDATE";
|
|
391
|
+
conversationId: string;
|
|
392
|
+
updates: Partial<ConversationSummary>;
|
|
393
|
+
};
|
|
394
|
+
type SyncEvent = NewMessageEvent | StatusUpdateEvent | ConversationUpdateEvent;
|
|
395
|
+
//#endregion
|
|
396
|
+
//#region src/logger.d.ts
|
|
397
|
+
/**
|
|
398
|
+
* Centralized Logger for Better Zap
|
|
399
|
+
*
|
|
400
|
+
* Lightweight, injectable logger with structured JSON output.
|
|
401
|
+
* Cloudflare Workers compatible (no external dependencies).
|
|
402
|
+
*/
|
|
403
|
+
type LogLevel = "debug" | "info" | "warn" | "error";
|
|
404
|
+
/** User-facing configuration (on betterZap config). */
|
|
405
|
+
interface LoggerConfig {
|
|
406
|
+
disabled?: boolean;
|
|
407
|
+
level?: LogLevel;
|
|
408
|
+
log?: (level: LogLevel, message: string, context: Record<string, unknown>) => void;
|
|
409
|
+
}
|
|
410
|
+
/** Internal logger interface injected into services. */
|
|
411
|
+
interface Logger {
|
|
412
|
+
debug(message: string, context?: Record<string, unknown>): void;
|
|
413
|
+
info(message: string, context?: Record<string, unknown>): void;
|
|
414
|
+
warn(message: string, context?: Record<string, unknown>): void;
|
|
415
|
+
error(message: string, context?: Record<string, unknown>): void;
|
|
416
|
+
}
|
|
417
|
+
declare function createLogger(config?: LoggerConfig): Logger;
|
|
418
|
+
declare const noopLogger: Logger;
|
|
419
|
+
declare function serializeError(err: unknown): Record<string, unknown>;
|
|
420
|
+
//#endregion
|
|
421
|
+
//#region src/services/message-logger.service.d.ts
|
|
422
|
+
type WhatsAppDirection = "incoming" | "outgoing";
|
|
423
|
+
type WhatsAppStatus = "sent" | "delivered" | "read" | "failed";
|
|
424
|
+
declare const WHATSAPP_MESSAGE_TYPES: readonly ["queue_position", "next_in_line", "queue_optin", "marketing", "bot_reply", "reminder", "satisfaction", "incoming"];
|
|
425
|
+
type WhatsAppMessageType = (typeof WHATSAPP_MESSAGE_TYPES)[number];
|
|
426
|
+
interface WhatsAppLogRecord {
|
|
427
|
+
id: string;
|
|
428
|
+
conversationId: string;
|
|
429
|
+
userId?: string | null;
|
|
430
|
+
phone: string;
|
|
431
|
+
waMessageId?: string | null;
|
|
432
|
+
direction: WhatsAppDirection;
|
|
433
|
+
messageType: WhatsAppMessageType;
|
|
434
|
+
content: string | null;
|
|
435
|
+
templateName?: string | null;
|
|
436
|
+
status: WhatsAppStatus;
|
|
437
|
+
errorMessage?: string | null;
|
|
438
|
+
metadata?: any;
|
|
439
|
+
sentAt: string;
|
|
440
|
+
deliveredAt?: string | null;
|
|
441
|
+
readAt?: string | null;
|
|
442
|
+
}
|
|
443
|
+
type CreateWhatsAppLogParams = {
|
|
444
|
+
phone: string;
|
|
445
|
+
userId?: string;
|
|
446
|
+
contactName?: string;
|
|
447
|
+
direction: WhatsAppDirection;
|
|
448
|
+
messageType: WhatsAppMessageType;
|
|
449
|
+
content: string;
|
|
450
|
+
templateName?: string;
|
|
451
|
+
waMessageId?: string;
|
|
452
|
+
status: WhatsAppStatus;
|
|
453
|
+
errorMessage?: string;
|
|
454
|
+
metadata?: any;
|
|
455
|
+
sentAt: string;
|
|
456
|
+
};
|
|
457
|
+
/**
|
|
458
|
+
* Interface for database persistence of WhatsApp logs.
|
|
459
|
+
* Decouples Better Zap from any specific application database package.
|
|
460
|
+
*/
|
|
461
|
+
interface WhatsAppLogStore {
|
|
462
|
+
createWhatsAppLog(params: CreateWhatsAppLogParams): Promise<{
|
|
463
|
+
record: WhatsAppLogRecord;
|
|
464
|
+
created: boolean;
|
|
465
|
+
}>;
|
|
466
|
+
getMessageByWaId(waMessageId: string): Promise<WhatsAppLogRecord | null>;
|
|
467
|
+
updateWhatsAppLogByWaMessageId(waMessageId: string, updates: Partial<WhatsAppLogRecord>): Promise<void>;
|
|
468
|
+
/**
|
|
469
|
+
* Atomically update status only if the new status advances the lifecycle.
|
|
470
|
+
* Returns true if the row was updated, false if skipped (duplicate/regression).
|
|
471
|
+
*
|
|
472
|
+
* Progression: sent(1) → delivered(2) → read(3). failed(4) always wins.
|
|
473
|
+
*/
|
|
474
|
+
updateStatusIfProgressed(waMessageId: string, newStatus: WhatsAppStatus, updates: Partial<WhatsAppLogRecord>): Promise<boolean>;
|
|
475
|
+
getConversationById(conversationId: string): Promise<ConversationRecord | null>;
|
|
476
|
+
getConversationByPhone(phone: string): Promise<ConversationRecord | null>;
|
|
477
|
+
getConversations(): Promise<ConversationRecord[]>;
|
|
478
|
+
getMessagesByConversationPaginated(conversationId: string, cursor?: string | null, limit?: number): Promise<Array<{
|
|
479
|
+
id: string;
|
|
480
|
+
phone?: string;
|
|
481
|
+
content: string | null;
|
|
482
|
+
direction: string;
|
|
483
|
+
status: string;
|
|
484
|
+
sentAt: string;
|
|
485
|
+
templateName?: string | null;
|
|
486
|
+
messageType?: string | null;
|
|
487
|
+
metadata?: any;
|
|
488
|
+
}>>;
|
|
489
|
+
/**
|
|
490
|
+
* Check if there's a recent outgoing message to this phone within N hours.
|
|
491
|
+
* Used for consumer-defined cooldown checks.
|
|
492
|
+
*/
|
|
493
|
+
hasRecentOutgoingMessage(phone: string, withinHours: number): Promise<boolean>;
|
|
494
|
+
}
|
|
495
|
+
interface MessageLoggerNotifier {
|
|
496
|
+
notify(event: SyncEvent): Promise<void>;
|
|
497
|
+
}
|
|
498
|
+
declare class MessageLoggerService {
|
|
499
|
+
private store;
|
|
500
|
+
private notifier?;
|
|
501
|
+
private log;
|
|
502
|
+
constructor(store: WhatsAppLogStore, log: Logger, notifier?: MessageLoggerNotifier | undefined);
|
|
503
|
+
private notify;
|
|
504
|
+
getConversationById(conversationId: string): Promise<Conversation | null>;
|
|
505
|
+
getConversationByPhone(phone: string): Promise<Conversation | null>;
|
|
506
|
+
getConversations(): Promise<Conversation[]>;
|
|
507
|
+
/** @deprecated Prefer `getFreeformMessageWindow()`. */
|
|
508
|
+
getCustomerCareWindow(phone: string): Promise<FreeformMessageWindow>;
|
|
509
|
+
getFreeformMessageWindow(phone: string): Promise<FreeformMessageWindow>;
|
|
510
|
+
/**
|
|
511
|
+
* Check if a message with this waMessageId was already processed.
|
|
512
|
+
*/
|
|
513
|
+
isDuplicate(waMessageId: string): Promise<boolean>;
|
|
514
|
+
/**
|
|
515
|
+
* Log outgoing message for LGPD compliance
|
|
516
|
+
*/
|
|
517
|
+
logOutgoing(params: {
|
|
518
|
+
phone: string;
|
|
519
|
+
userId?: string;
|
|
520
|
+
messageType: WhatsAppMessageType;
|
|
521
|
+
content: string;
|
|
522
|
+
result: SendResult;
|
|
523
|
+
templateName?: string;
|
|
524
|
+
metadata?: Record<string, any>;
|
|
525
|
+
}): Promise<string>;
|
|
526
|
+
/**
|
|
527
|
+
* Update message status from webhook callback.
|
|
528
|
+
* Only applies if the new status advances the lifecycle (atomic, no race conditions).
|
|
529
|
+
* Returns true if the update was applied, false if skipped.
|
|
530
|
+
*/
|
|
531
|
+
updateStatus(waMessageId: string, status: WhatsAppStatus, timestamp: string, errorMessage?: string): Promise<boolean>;
|
|
532
|
+
/**
|
|
533
|
+
* Check if there's a recent outgoing message to this phone within N hours.
|
|
534
|
+
*
|
|
535
|
+
* @param {string} phone : The phone number to check (in E.164 format)
|
|
536
|
+
* @param {number} [withinHours=24] : Time in hours to look for incoming messages
|
|
537
|
+
*/
|
|
538
|
+
hasRecentOutgoingMessage(phone: string, withinHours?: number): Promise<boolean>;
|
|
539
|
+
/**
|
|
540
|
+
* Log incoming message (for audit trail)
|
|
541
|
+
*/
|
|
542
|
+
logIncoming(params: {
|
|
543
|
+
phone: string;
|
|
544
|
+
waMessageId: string;
|
|
545
|
+
content: string;
|
|
546
|
+
sentAt: string;
|
|
547
|
+
senderName?: string;
|
|
548
|
+
metadata?: Record<string, unknown>;
|
|
549
|
+
}): Promise<boolean>;
|
|
550
|
+
/**
|
|
551
|
+
* Log an imported WhatsApp message with an explicit direction and timestamp.
|
|
552
|
+
* Used by coexistence history imports and app echo webhooks where the message
|
|
553
|
+
* did not originate from the local send API call.
|
|
554
|
+
*/
|
|
555
|
+
logImportedMessage(params: {
|
|
556
|
+
phone: string;
|
|
557
|
+
waMessageId: string;
|
|
558
|
+
direction: WhatsAppDirection;
|
|
559
|
+
content: string;
|
|
560
|
+
sentAt: string;
|
|
561
|
+
senderName?: string;
|
|
562
|
+
messageType?: WhatsAppMessageType;
|
|
563
|
+
metadata?: Record<string, unknown>;
|
|
564
|
+
}): Promise<boolean>;
|
|
565
|
+
}
|
|
566
|
+
//#endregion
|
|
567
|
+
//#region src/services/whatsapp.service.d.ts
|
|
568
|
+
interface OutgoingLoggingMetadata {
|
|
569
|
+
userId?: string;
|
|
570
|
+
messageType: WhatsAppMessageType;
|
|
571
|
+
/** Human-readable content of the message to be stored in the log */
|
|
572
|
+
content: string;
|
|
573
|
+
/** Optional metadata for the log entry (e.g. campaign ID, additional context) */
|
|
574
|
+
metadata?: Record<string, any>;
|
|
575
|
+
}
|
|
576
|
+
declare class WhatsAppService {
|
|
577
|
+
private baseUrl;
|
|
578
|
+
private token;
|
|
579
|
+
private isDev;
|
|
580
|
+
private logger;
|
|
581
|
+
private log;
|
|
582
|
+
constructor(config: WhatsAppConfig, logger: MessageLoggerService, log: Logger);
|
|
583
|
+
/** Send a text message within the 24h free-form message window only. */
|
|
584
|
+
sendText(to: string, body: string, logging?: Omit<OutgoingLoggingMetadata, "content">): Promise<SendResult>;
|
|
585
|
+
/** Send a template message (works outside service window). */
|
|
586
|
+
sendTemplate(to: string, templateName: string, languageCode?: string, components?: TemplateComponent[], logging?: OutgoingLoggingMetadata): Promise<SendResult>;
|
|
587
|
+
/** Send an interactive message with reply buttons (up to 3). */
|
|
588
|
+
sendInteractiveButtons(to: string, bodyText: string, buttons: Array<{
|
|
589
|
+
id: string;
|
|
590
|
+
title: string;
|
|
591
|
+
}>, logging?: Omit<OutgoingLoggingMetadata, "content">): Promise<SendResult>;
|
|
592
|
+
/** Send an interactive list message with sections and rows. */
|
|
593
|
+
sendInteractiveList(to: string, bodyText: string, buttonLabel: string, sections: Array<{
|
|
594
|
+
title: string;
|
|
595
|
+
rows: Array<{
|
|
596
|
+
id: string;
|
|
597
|
+
title: string;
|
|
598
|
+
description?: string;
|
|
599
|
+
}>;
|
|
600
|
+
}>, logging?: Omit<OutgoingLoggingMetadata, "content">): Promise<SendResult>;
|
|
601
|
+
/** Send an interactive media carousel message (2-10 cards). */
|
|
602
|
+
sendInteractiveMediaCarousel(data: SendInteractiveMediaCarouselData, logging?: Omit<OutgoingLoggingMetadata, "content">): Promise<SendResult>;
|
|
603
|
+
/** Send a location pin message. */
|
|
604
|
+
sendLocation(to: string, latitude: number, longitude: number, name: string, address: string, logging?: Omit<OutgoingLoggingMetadata, "content">): Promise<SendResult>;
|
|
605
|
+
/**
|
|
606
|
+
* Mark an inbound message as read.
|
|
607
|
+
*
|
|
608
|
+
* @see https://developers.facebook.com/docs/whatsapp/cloud-api/messages/mark-messages-as-read
|
|
609
|
+
*/
|
|
610
|
+
markAsRead(messageId: string): Promise<SendResult>;
|
|
611
|
+
/**
|
|
612
|
+
* Show or hide a typing indicator in the chat.
|
|
613
|
+
* When starting, the indicator auto-dismisses after 25 seconds or when a message is sent.
|
|
614
|
+
*
|
|
615
|
+
* @see https://developers.facebook.com/docs/whatsapp/cloud-api/typing-indicators/
|
|
616
|
+
*/
|
|
617
|
+
typingIndicator(messageId: string, action?: "typing_on" | "typing_off"): Promise<SendResult>;
|
|
618
|
+
/**
|
|
619
|
+
* Add a reaction to a message.
|
|
620
|
+
*
|
|
621
|
+
* @see https://developers.facebook.com/docs/whatsapp/cloud-api/messages/reaction-messages
|
|
622
|
+
*/
|
|
623
|
+
sendReaction(to: string, messageId: string, emoji: string): Promise<SendResult>;
|
|
624
|
+
/** Core send method with retry logic (2 retries, exponential backoff). */
|
|
625
|
+
private send;
|
|
626
|
+
private logSendResult;
|
|
627
|
+
/** Actually performs the network request with retries. */
|
|
628
|
+
private performRequest;
|
|
629
|
+
}
|
|
630
|
+
//#endregion
|
|
631
|
+
//#region src/template-registry.d.ts
|
|
632
|
+
type Simplify<TValue> = { [TKey in keyof TValue]: TValue[TKey] } & {};
|
|
633
|
+
type TemplateParameterInputMap = {
|
|
634
|
+
text: string;
|
|
635
|
+
payload: string;
|
|
636
|
+
location: {
|
|
637
|
+
latitude: number;
|
|
638
|
+
longitude: number;
|
|
639
|
+
name: string;
|
|
640
|
+
address: string;
|
|
641
|
+
};
|
|
642
|
+
image: {
|
|
643
|
+
link: string;
|
|
644
|
+
};
|
|
645
|
+
video: {
|
|
646
|
+
link: string;
|
|
647
|
+
};
|
|
648
|
+
document: {
|
|
649
|
+
link: string;
|
|
650
|
+
};
|
|
651
|
+
currency: {
|
|
652
|
+
fallback_value: string;
|
|
653
|
+
code: string;
|
|
654
|
+
amount_1000: number;
|
|
655
|
+
};
|
|
656
|
+
date_time: {
|
|
657
|
+
fallback_value: string;
|
|
658
|
+
};
|
|
659
|
+
};
|
|
660
|
+
type SupportedTemplateParameterType = keyof TemplateParameterInputMap;
|
|
661
|
+
interface TemplateParameterDefinition<TType extends SupportedTemplateParameterType = SupportedTemplateParameterType> {
|
|
662
|
+
name: string;
|
|
663
|
+
parameterName?: string;
|
|
664
|
+
type: TType;
|
|
665
|
+
}
|
|
666
|
+
type TemplateComponentDefinition = {
|
|
667
|
+
type: "header";
|
|
668
|
+
parameters: readonly TemplateParameterDefinition[];
|
|
669
|
+
} | {
|
|
670
|
+
type: "body";
|
|
671
|
+
parameters: readonly TemplateParameterDefinition[];
|
|
672
|
+
} | {
|
|
673
|
+
type: "button";
|
|
674
|
+
subType: "url" | "quick_reply";
|
|
675
|
+
index: string;
|
|
676
|
+
parameters: readonly TemplateParameterDefinition[];
|
|
677
|
+
};
|
|
678
|
+
interface TemplateDefinition {
|
|
679
|
+
language: string;
|
|
680
|
+
components?: readonly TemplateComponentDefinition[];
|
|
681
|
+
}
|
|
682
|
+
type TemplateRegistry = Record<string, TemplateDefinition>;
|
|
683
|
+
type TemplateParameterDefinitionUnion<TTemplate extends TemplateDefinition> = TTemplate["components"] extends readonly TemplateComponentDefinition[] ? TTemplate["components"][number]["parameters"][number] : never;
|
|
684
|
+
type TemplateParameterInput<TParameter extends TemplateParameterDefinition> = TParameter["type"] extends keyof TemplateParameterInputMap ? TemplateParameterInputMap[TParameter["type"]] : never;
|
|
685
|
+
type TemplateParams<TTemplate extends TemplateDefinition> = Simplify<{ [TParameter in TemplateParameterDefinitionUnion<TTemplate> as TParameter["name"]]: TemplateParameterInput<TParameter> }>;
|
|
686
|
+
type TemplateName<TTemplates extends TemplateRegistry> = Extract<keyof TTemplates, string>;
|
|
687
|
+
type TypedTemplateOptions<TTemplates extends TemplateRegistry, TName extends TemplateName<TTemplates>> = {
|
|
688
|
+
language?: string;
|
|
689
|
+
params: TemplateParams<TTemplates[TName]>;
|
|
690
|
+
};
|
|
691
|
+
declare const EMPTY_TEMPLATE_REGISTRY: {};
|
|
692
|
+
declare function defineTemplates<const TTemplates extends TemplateRegistry>(templates: TTemplates): TTemplates;
|
|
693
|
+
declare function hasConfiguredTemplates<TTemplates extends TemplateRegistry>(templates: TTemplates): boolean;
|
|
694
|
+
declare function getTemplateNames<TTemplates extends TemplateRegistry>(templates: TTemplates): TemplateName<TTemplates>[];
|
|
695
|
+
declare function serializeTemplateFromRegistry(templates: TemplateRegistry, templateName: string, options: {
|
|
696
|
+
language?: string;
|
|
697
|
+
params: Record<string, unknown>;
|
|
698
|
+
}): {
|
|
699
|
+
language: string;
|
|
700
|
+
components?: TemplateComponent[];
|
|
701
|
+
};
|
|
702
|
+
declare function serializeTemplateFromRegistry<TTemplates extends TemplateRegistry, TName extends TemplateName<TTemplates>>(templates: TTemplates, templateName: TName, options: TypedTemplateOptions<TTemplates, TName>): {
|
|
703
|
+
language: string;
|
|
704
|
+
components?: TemplateComponent[];
|
|
705
|
+
};
|
|
706
|
+
//#endregion
|
|
707
|
+
//#region src/client.d.ts
|
|
708
|
+
interface ZapClientOptions<TTemplates extends TemplateRegistry = {}> {
|
|
709
|
+
/**
|
|
710
|
+
* Base URL for the API. Defaults to `window.location.origin` in the browser.
|
|
711
|
+
* Useful for SSR or custom proxy setups.
|
|
712
|
+
*/
|
|
713
|
+
baseURL?: string;
|
|
714
|
+
/**
|
|
715
|
+
* Base path for all routes. Must match the `basePath` used in `betterZap()`.
|
|
716
|
+
* @default "/api/whatsapp"
|
|
717
|
+
*/
|
|
718
|
+
basePath?: string;
|
|
719
|
+
/**
|
|
720
|
+
* Custom fetch implementation. Defaults to the global `fetch`.
|
|
721
|
+
*/
|
|
722
|
+
fetch?: typeof fetch;
|
|
723
|
+
/**
|
|
724
|
+
* How requests authenticate to the Better Zap HTTP routes. Defaults to
|
|
725
|
+
* sending requests as-is (ambient same-origin cookies). See {@link ZapTransport}
|
|
726
|
+
* and the {@link sessionTransport} / {@link apiKeyTransport} built-ins.
|
|
727
|
+
*/
|
|
728
|
+
transport?: ZapTransport;
|
|
729
|
+
/**
|
|
730
|
+
* Optional template registry used only for type inference.
|
|
731
|
+
* The HTTP server still owns runtime serialization and validation.
|
|
732
|
+
*/
|
|
733
|
+
templates?: TTemplates;
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Controls how the client authenticates its requests to the Better Zap HTTP
|
|
737
|
+
* routes. This is the seam that lets a browser client talk to the API without
|
|
738
|
+
* embedding an API key: the app proxies Better Zap behind its own
|
|
739
|
+
* session-authenticated routes and the client just forwards the session.
|
|
740
|
+
*
|
|
741
|
+
* Two built-ins cover the common cases:
|
|
742
|
+
* - {@link sessionTransport} — session/proxy auth: sends same-origin
|
|
743
|
+
* credentials (cookies) and no API key. Use when a trusted server owns the
|
|
744
|
+
* session and proxies Better Zap (e.g. a dashboard's own authed routes).
|
|
745
|
+
* - {@link apiKeyTransport} — sends `Authorization: Bearer <key>`. Use for
|
|
746
|
+
* server-to-server calls in a trusted environment.
|
|
747
|
+
*
|
|
748
|
+
* Implement the interface directly for anything else (e.g. a rotating token).
|
|
749
|
+
*/
|
|
750
|
+
interface ZapTransport {
|
|
751
|
+
/** Extra headers merged onto every request. Called per request, may be async. */
|
|
752
|
+
headers?(): Record<string, string> | Promise<Record<string, string>>;
|
|
753
|
+
/** Fetch credentials mode; `sessionTransport` sets `"include"`. */
|
|
754
|
+
credentials?: RequestCredentials;
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Session/proxy transport: send same-origin credentials (cookies), no API key.
|
|
758
|
+
* The recommended browser default when the app proxies Better Zap behind its
|
|
759
|
+
* own authenticated routes.
|
|
760
|
+
*/
|
|
761
|
+
declare function sessionTransport(): ZapTransport;
|
|
762
|
+
/** API-key transport: send `Authorization: Bearer <apiKey>` on every request. */
|
|
763
|
+
declare function apiKeyTransport(apiKey: string): ZapTransport;
|
|
764
|
+
declare class BetterZapClientError extends Error {
|
|
765
|
+
status: number;
|
|
766
|
+
code?: string;
|
|
767
|
+
details?: unknown;
|
|
768
|
+
body?: unknown;
|
|
769
|
+
constructor(message: string, options: {
|
|
770
|
+
status: number;
|
|
771
|
+
code?: string;
|
|
772
|
+
details?: unknown;
|
|
773
|
+
body?: unknown;
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
interface SendTextParams {
|
|
777
|
+
to: string;
|
|
778
|
+
body: string;
|
|
779
|
+
messageType?: string;
|
|
780
|
+
userId?: string;
|
|
781
|
+
metadata?: Record<string, unknown>;
|
|
782
|
+
}
|
|
783
|
+
interface SendTemplateRawParams {
|
|
784
|
+
to: string;
|
|
785
|
+
template: string;
|
|
786
|
+
language?: string;
|
|
787
|
+
components?: unknown[];
|
|
788
|
+
logging?: OutgoingLoggingMetadata;
|
|
789
|
+
}
|
|
790
|
+
type SendTemplateTypedParams<TTemplates extends TemplateRegistry, TName extends TemplateName<TTemplates>> = {
|
|
791
|
+
to: string;
|
|
792
|
+
template: TName;
|
|
793
|
+
language?: string;
|
|
794
|
+
params: TemplateParams<TTemplates[TName]>;
|
|
795
|
+
logging?: OutgoingLoggingMetadata;
|
|
796
|
+
};
|
|
797
|
+
type SendTemplateMethod<TTemplates extends TemplateRegistry> = [TemplateName<TTemplates>] extends [never] ? (params: SendTemplateRawParams) => Promise<SendResult> : <TName extends TemplateName<TTemplates>>(params: SendTemplateTypedParams<TTemplates, TName>) => Promise<SendResult>;
|
|
798
|
+
interface SendInteractiveParams {
|
|
799
|
+
to: string;
|
|
800
|
+
type: "button" | "list" | "carousel";
|
|
801
|
+
body: string;
|
|
802
|
+
buttons?: Array<{
|
|
803
|
+
id: string;
|
|
804
|
+
title: string;
|
|
805
|
+
}>;
|
|
806
|
+
buttonLabel?: string;
|
|
807
|
+
sections?: Array<{
|
|
808
|
+
title: string;
|
|
809
|
+
rows: Array<{
|
|
810
|
+
id: string;
|
|
811
|
+
title: string;
|
|
812
|
+
description?: string;
|
|
813
|
+
}>;
|
|
814
|
+
}>;
|
|
815
|
+
cards?: SendInteractiveMediaCarouselData["cards"];
|
|
816
|
+
messageType?: string;
|
|
817
|
+
userId?: string;
|
|
818
|
+
metadata?: Record<string, unknown>;
|
|
819
|
+
}
|
|
820
|
+
interface SendLocationParams {
|
|
821
|
+
to: string;
|
|
822
|
+
latitude: number;
|
|
823
|
+
longitude: number;
|
|
824
|
+
name: string;
|
|
825
|
+
address: string;
|
|
826
|
+
messageType?: string;
|
|
827
|
+
userId?: string;
|
|
828
|
+
metadata?: Record<string, unknown>;
|
|
829
|
+
}
|
|
830
|
+
interface GetMessagesOptions {
|
|
831
|
+
cursor?: string;
|
|
832
|
+
limit?: number;
|
|
833
|
+
}
|
|
834
|
+
interface ZapClient<TTemplates extends TemplateRegistry = {}> {
|
|
835
|
+
send: {
|
|
836
|
+
text(params: SendTextParams): Promise<SendResult>;
|
|
837
|
+
template: SendTemplateMethod<TTemplates>;
|
|
838
|
+
templateRaw(params: SendTemplateRawParams): Promise<SendResult>;
|
|
839
|
+
interactive(params: SendInteractiveParams): Promise<SendResult>;
|
|
840
|
+
location(params: SendLocationParams): Promise<SendResult>;
|
|
841
|
+
};
|
|
842
|
+
conversations: {
|
|
843
|
+
list(): Promise<Conversation[]>;
|
|
844
|
+
get(phone: string): Promise<Conversation | null>;
|
|
845
|
+
messages(phone: string, opts?: GetMessagesOptions): Promise<UIMessage[]>;
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
declare function createZapClient<TTemplates extends TemplateRegistry = {}>(options?: ZapClientOptions<TTemplates>): ZapClient<TTemplates>;
|
|
849
|
+
//#endregion
|
|
850
|
+
export { SendResult as $, LogLevel as A, SyncEvent as B, MessageLoggerService as C, WhatsAppLogStore as D, WhatsAppLogRecord as E, serializeError as F, IncomingMessage as G, Conversation as H, ConversationSummary as I, MessageError as J, InteractiveMediaCarouselCardInput as K, ConversationUpdateEvent as L, LoggerConfig as M, createLogger as N, WhatsAppMessageType as O, noopLogger as P, SendMessageResponse as Q, NewMessageEvent as R, MessageLoggerNotifier as S, WhatsAppDirection as T, ConversationRecord as U, WhatsAppConfig as V, FreeformMessageWindow as W, SendInteractiveMediaCarouselData as X, MessageStatus as Y, SendMessageError as Z, getTemplateNames as _, WhatsAppWebhookField as _t, createZapClient as a, WebhookContact as at, OutgoingLoggingMetadata as b, SupportedTemplateParameterType as c, WebhookPayload as ct, TemplateName as d, WhatsAppInteractiveButtonsMessage as dt, TemplateComponent as et, TemplateParameterDefinition as f, WhatsAppInteractiveListMessage as ft, defineTemplates as g, WhatsAppTextMessage as gt, TemplateRegistry as h, WhatsAppTemplateMessage as ht, apiKeyTransport as i, WebhookChange as it, Logger as j, WhatsAppStatus as k, TemplateComponentDefinition as l, WebhookValue as lt, TemplateParams as m, WhatsAppLocationMessage as mt, ZapClient as n, UIMessage as nt, sessionTransport as o, WebhookEntry as ot, TemplateParameterInputMap as p, WhatsAppInteractiveMediaCarouselMessage as pt, MediaMessage as q, ZapTransport as r, UIMessageStatus as rt, EMPTY_TEMPLATE_REGISTRY as s, WebhookError as st, BetterZapClientError as t, TemplateParameter as tt, TemplateDefinition as u, WhatsAppCarouselCard as ut, hasConfiguredTemplates as v, WHATSAPP_MESSAGE_TYPES as w, WhatsAppService as x, serializeTemplateFromRegistry as y, StatusUpdateEvent as z };
|