better-zap 0.0.1 → 0.0.2

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.
@@ -317,9 +317,16 @@ interface SendResult {
317
317
  messageId?: string;
318
318
  error?: string;
319
319
  errorCode?: number;
320
+ code?: string;
320
321
  httpStatus?: number;
322
+ details?: Record<string, any>;
321
323
  }
322
- type Conversation = {
324
+ type FreeformMessageWindow = {
325
+ isOpen: boolean;
326
+ lastIncomingMessageAt: string | null;
327
+ expiresAt: string | null;
328
+ };
329
+ type ConversationRecord = {
323
330
  id: string;
324
331
  phone: string;
325
332
  contactName: string | null;
@@ -331,6 +338,9 @@ type Conversation = {
331
338
  messageCount: number;
332
339
  lastIncomingMessageAt: string | null;
333
340
  };
341
+ type Conversation = ConversationRecord & {
342
+ freeformMessageWindow: FreeformMessageWindow;
343
+ };
334
344
  type UIMessageStatus = "sent" | "delivered" | "read" | "failed";
335
345
  type UIMessage = {
336
346
  id: string;
@@ -452,19 +462,9 @@ interface WhatsAppLogStore {
452
462
  * Progression: sent(1) → delivered(2) → read(3). failed(4) always wins.
453
463
  */
454
464
  updateStatusIfProgressed(waMessageId: string, newStatus: WhatsAppStatus, updates: Partial<WhatsAppLogRecord>): Promise<boolean>;
455
- getConversationById(conversationId: string): Promise<ConversationSummary | null>;
456
- getConversationByPhone(phone: string): Promise<ConversationSummary | null>;
457
- getConversations(): Promise<Array<{
458
- id: string;
459
- phone: string;
460
- contactName: string | null;
461
- unreadCount: number;
462
- status: string;
463
- lastMessageAt: string;
464
- lastMessagePreview: string | null;
465
- lastDirection: string;
466
- messageCount: number;
467
- }>>;
465
+ getConversationById(conversationId: string): Promise<ConversationRecord | null>;
466
+ getConversationByPhone(phone: string): Promise<ConversationRecord | null>;
467
+ getConversations(): Promise<ConversationRecord[]>;
468
468
  getMessagesByConversationPaginated(conversationId: string, cursor?: string | null, limit?: number): Promise<Array<{
469
469
  id: string;
470
470
  phone?: string;
@@ -478,7 +478,7 @@ interface WhatsAppLogStore {
478
478
  }>>;
479
479
  /**
480
480
  * Check if there's a recent outgoing message to this phone within N hours.
481
- * Used to approximate Meta's 24h conversation window and for cooldown checks.
481
+ * Used for consumer-defined cooldown checks.
482
482
  */
483
483
  hasRecentOutgoingMessage(phone: string, withinHours: number): Promise<boolean>;
484
484
  }
@@ -491,6 +491,12 @@ declare class MessageLoggerService {
491
491
  private log;
492
492
  constructor(store: WhatsAppLogStore, log: Logger, notifier?: MessageLoggerNotifier | undefined);
493
493
  private notify;
494
+ getConversationById(conversationId: string): Promise<Conversation | null>;
495
+ getConversationByPhone(phone: string): Promise<Conversation | null>;
496
+ getConversations(): Promise<Conversation[]>;
497
+ /** @deprecated Prefer `getFreeformMessageWindow()`. */
498
+ getCustomerCareWindow(phone: string): Promise<FreeformMessageWindow>;
499
+ getFreeformMessageWindow(phone: string): Promise<FreeformMessageWindow>;
494
500
  /**
495
501
  * Check if a message with this waMessageId was already processed.
496
502
  */
@@ -527,6 +533,7 @@ declare class MessageLoggerService {
527
533
  phone: string;
528
534
  waMessageId: string;
529
535
  content: string;
536
+ sentAt: string;
530
537
  senderName?: string;
531
538
  metadata?: Record<string, unknown>;
532
539
  }): Promise<void>;
@@ -548,7 +555,7 @@ declare class WhatsAppService {
548
555
  private logger;
549
556
  private log;
550
557
  constructor(config: WhatsAppConfig, logger: MessageLoggerService, log: Logger);
551
- /** Send a text message (within 24h service window only). */
558
+ /** Send a text message within the 24h free-form message window only. */
552
559
  sendText(to: string, body: string, logging?: Omit<OutgoingLoggingMetadata, "content">): Promise<SendResult>;
553
560
  /** Send a template message (works outside service window). */
554
561
  sendTemplate(to: string, templateName: string, languageCode?: string, components?: TemplateComponent[], logging?: OutgoingLoggingMetadata): Promise<SendResult>;
@@ -591,6 +598,7 @@ declare class WhatsAppService {
591
598
  sendReaction(to: string, messageId: string, emoji: string): Promise<SendResult>;
592
599
  /** Core send method with retry logic (2 retries, exponential backoff). */
593
600
  private send;
601
+ private logSendResult;
594
602
  /** Actually performs the network request with retries. */
595
603
  private performRequest;
596
604
  }
@@ -692,6 +700,18 @@ interface ZapClientOptions<TTemplates extends TemplateRegistry = {}> {
692
700
  */
693
701
  templates?: TTemplates;
694
702
  }
703
+ declare class BetterZapClientError extends Error {
704
+ status: number;
705
+ code?: string;
706
+ details?: unknown;
707
+ body?: unknown;
708
+ constructor(message: string, options: {
709
+ status: number;
710
+ code?: string;
711
+ details?: unknown;
712
+ body?: unknown;
713
+ });
714
+ }
695
715
  interface SendTextParams {
696
716
  to: string;
697
717
  body: string;
@@ -766,4 +786,4 @@ interface ZapClient<TTemplates extends TemplateRegistry = {}> {
766
786
  }
767
787
  declare function createZapClient<TTemplates extends TemplateRegistry = {}>(options?: ZapClientOptions<TTemplates>): ZapClient<TTemplates>;
768
788
  //#endregion
769
- export { WebhookContact as $, noopLogger as A, InteractiveMediaCarouselCardInput as B, WhatsAppLogStore as C, Logger as D, LogLevel as E, StatusUpdateEvent as F, SendMessageError as G, MessageError as H, SyncEvent as I, TemplateComponent as J, SendMessageResponse as K, WhatsAppConfig as L, ConversationSummary as M, ConversationUpdateEvent as N, LoggerConfig as O, NewMessageEvent as P, WebhookChange as Q, Conversation as R, WhatsAppLogRecord as S, WhatsAppStatus as T, MessageStatus as U, MediaMessage as V, SendInteractiveMediaCarouselData as W, UIMessage as X, TemplateParameter as Y, UIMessageStatus as Z, WhatsAppService as _, TemplateComponentDefinition as a, WhatsAppInteractiveButtonsMessage as at, WHATSAPP_MESSAGE_TYPES as b, TemplateParameterDefinition as c, WhatsAppLocationMessage as ct, TemplateRegistry as d, WebhookEntry as et, defineTemplates as f, OutgoingLoggingMetadata as g, serializeTemplateFromRegistry as h, SupportedTemplateParameterType as i, WhatsAppCarouselCard as it, serializeError as j, createLogger as k, TemplateParameterInputMap as l, WhatsAppTemplateMessage as lt, hasConfiguredTemplates as m, createZapClient as n, WebhookPayload as nt, TemplateDefinition as o, WhatsAppInteractiveListMessage as ot, getTemplateNames as p, SendResult as q, EMPTY_TEMPLATE_REGISTRY as r, WebhookValue as rt, TemplateName as s, WhatsAppInteractiveMediaCarouselMessage as st, ZapClient as t, WebhookError as tt, TemplateParams as u, WhatsAppTextMessage as ut, MessageLoggerNotifier as v, WhatsAppMessageType as w, WhatsAppDirection as x, MessageLoggerService as y, IncomingMessage as z };
789
+ export { UIMessage as $, createLogger as A, ConversationRecord as B, WhatsAppLogRecord as C, LogLevel as D, WhatsAppStatus as E, NewMessageEvent as F, MessageError as G, IncomingMessage as H, StatusUpdateEvent as I, SendMessageError as J, MessageStatus as K, SyncEvent as L, serializeError as M, ConversationSummary as N, Logger as O, ConversationUpdateEvent as P, TemplateParameter as Q, WhatsAppConfig as R, WhatsAppDirection as S, WhatsAppMessageType as T, InteractiveMediaCarouselCardInput as U, FreeformMessageWindow as V, MediaMessage as W, SendResult as X, SendMessageResponse as Y, TemplateComponent as Z, OutgoingLoggingMetadata as _, SupportedTemplateParameterType as a, WebhookPayload as at, MessageLoggerService as b, TemplateName as c, WhatsAppInteractiveButtonsMessage as ct, TemplateParams as d, WhatsAppLocationMessage as dt, UIMessageStatus as et, TemplateRegistry as f, WhatsAppTemplateMessage as ft, serializeTemplateFromRegistry as g, hasConfiguredTemplates as h, EMPTY_TEMPLATE_REGISTRY as i, WebhookError as it, noopLogger as j, LoggerConfig as k, TemplateParameterDefinition as l, WhatsAppInteractiveListMessage as lt, getTemplateNames as m, ZapClient as n, WebhookContact as nt, TemplateComponentDefinition as o, WebhookValue as ot, defineTemplates as p, WhatsAppTextMessage as pt, SendInteractiveMediaCarouselData as q, createZapClient as r, WebhookEntry as rt, TemplateDefinition as s, WhatsAppCarouselCard as st, BetterZapClientError as t, WebhookChange as tt, TemplateParameterInputMap as u, WhatsAppInteractiveMediaCarouselMessage as ut, WhatsAppService as v, WhatsAppLogStore as w, WHATSAPP_MESSAGE_TYPES as x, MessageLoggerNotifier as y, Conversation as z };
@@ -317,9 +317,16 @@ interface SendResult {
317
317
  messageId?: string;
318
318
  error?: string;
319
319
  errorCode?: number;
320
+ code?: string;
320
321
  httpStatus?: number;
322
+ details?: Record<string, any>;
321
323
  }
322
- type Conversation = {
324
+ type FreeformMessageWindow = {
325
+ isOpen: boolean;
326
+ lastIncomingMessageAt: string | null;
327
+ expiresAt: string | null;
328
+ };
329
+ type ConversationRecord = {
323
330
  id: string;
324
331
  phone: string;
325
332
  contactName: string | null;
@@ -331,6 +338,9 @@ type Conversation = {
331
338
  messageCount: number;
332
339
  lastIncomingMessageAt: string | null;
333
340
  };
341
+ type Conversation = ConversationRecord & {
342
+ freeformMessageWindow: FreeformMessageWindow;
343
+ };
334
344
  type UIMessageStatus = "sent" | "delivered" | "read" | "failed";
335
345
  type UIMessage = {
336
346
  id: string;
@@ -452,19 +462,9 @@ interface WhatsAppLogStore {
452
462
  * Progression: sent(1) → delivered(2) → read(3). failed(4) always wins.
453
463
  */
454
464
  updateStatusIfProgressed(waMessageId: string, newStatus: WhatsAppStatus, updates: Partial<WhatsAppLogRecord>): Promise<boolean>;
455
- getConversationById(conversationId: string): Promise<ConversationSummary | null>;
456
- getConversationByPhone(phone: string): Promise<ConversationSummary | null>;
457
- getConversations(): Promise<Array<{
458
- id: string;
459
- phone: string;
460
- contactName: string | null;
461
- unreadCount: number;
462
- status: string;
463
- lastMessageAt: string;
464
- lastMessagePreview: string | null;
465
- lastDirection: string;
466
- messageCount: number;
467
- }>>;
465
+ getConversationById(conversationId: string): Promise<ConversationRecord | null>;
466
+ getConversationByPhone(phone: string): Promise<ConversationRecord | null>;
467
+ getConversations(): Promise<ConversationRecord[]>;
468
468
  getMessagesByConversationPaginated(conversationId: string, cursor?: string | null, limit?: number): Promise<Array<{
469
469
  id: string;
470
470
  phone?: string;
@@ -478,7 +478,7 @@ interface WhatsAppLogStore {
478
478
  }>>;
479
479
  /**
480
480
  * Check if there's a recent outgoing message to this phone within N hours.
481
- * Used to approximate Meta's 24h conversation window and for cooldown checks.
481
+ * Used for consumer-defined cooldown checks.
482
482
  */
483
483
  hasRecentOutgoingMessage(phone: string, withinHours: number): Promise<boolean>;
484
484
  }
@@ -491,6 +491,12 @@ declare class MessageLoggerService {
491
491
  private log;
492
492
  constructor(store: WhatsAppLogStore, log: Logger, notifier?: MessageLoggerNotifier | undefined);
493
493
  private notify;
494
+ getConversationById(conversationId: string): Promise<Conversation | null>;
495
+ getConversationByPhone(phone: string): Promise<Conversation | null>;
496
+ getConversations(): Promise<Conversation[]>;
497
+ /** @deprecated Prefer `getFreeformMessageWindow()`. */
498
+ getCustomerCareWindow(phone: string): Promise<FreeformMessageWindow>;
499
+ getFreeformMessageWindow(phone: string): Promise<FreeformMessageWindow>;
494
500
  /**
495
501
  * Check if a message with this waMessageId was already processed.
496
502
  */
@@ -527,6 +533,7 @@ declare class MessageLoggerService {
527
533
  phone: string;
528
534
  waMessageId: string;
529
535
  content: string;
536
+ sentAt: string;
530
537
  senderName?: string;
531
538
  metadata?: Record<string, unknown>;
532
539
  }): Promise<void>;
@@ -548,7 +555,7 @@ declare class WhatsAppService {
548
555
  private logger;
549
556
  private log;
550
557
  constructor(config: WhatsAppConfig, logger: MessageLoggerService, log: Logger);
551
- /** Send a text message (within 24h service window only). */
558
+ /** Send a text message within the 24h free-form message window only. */
552
559
  sendText(to: string, body: string, logging?: Omit<OutgoingLoggingMetadata, "content">): Promise<SendResult>;
553
560
  /** Send a template message (works outside service window). */
554
561
  sendTemplate(to: string, templateName: string, languageCode?: string, components?: TemplateComponent[], logging?: OutgoingLoggingMetadata): Promise<SendResult>;
@@ -591,6 +598,7 @@ declare class WhatsAppService {
591
598
  sendReaction(to: string, messageId: string, emoji: string): Promise<SendResult>;
592
599
  /** Core send method with retry logic (2 retries, exponential backoff). */
593
600
  private send;
601
+ private logSendResult;
594
602
  /** Actually performs the network request with retries. */
595
603
  private performRequest;
596
604
  }
@@ -692,6 +700,18 @@ interface ZapClientOptions<TTemplates extends TemplateRegistry = {}> {
692
700
  */
693
701
  templates?: TTemplates;
694
702
  }
703
+ declare class BetterZapClientError extends Error {
704
+ status: number;
705
+ code?: string;
706
+ details?: unknown;
707
+ body?: unknown;
708
+ constructor(message: string, options: {
709
+ status: number;
710
+ code?: string;
711
+ details?: unknown;
712
+ body?: unknown;
713
+ });
714
+ }
695
715
  interface SendTextParams {
696
716
  to: string;
697
717
  body: string;
@@ -766,4 +786,4 @@ interface ZapClient<TTemplates extends TemplateRegistry = {}> {
766
786
  }
767
787
  declare function createZapClient<TTemplates extends TemplateRegistry = {}>(options?: ZapClientOptions<TTemplates>): ZapClient<TTemplates>;
768
788
  //#endregion
769
- export { WebhookContact as $, noopLogger as A, InteractiveMediaCarouselCardInput as B, WhatsAppLogStore as C, Logger as D, LogLevel as E, StatusUpdateEvent as F, SendMessageError as G, MessageError as H, SyncEvent as I, TemplateComponent as J, SendMessageResponse as K, WhatsAppConfig as L, ConversationSummary as M, ConversationUpdateEvent as N, LoggerConfig as O, NewMessageEvent as P, WebhookChange as Q, Conversation as R, WhatsAppLogRecord as S, WhatsAppStatus as T, MessageStatus as U, MediaMessage as V, SendInteractiveMediaCarouselData as W, UIMessage as X, TemplateParameter as Y, UIMessageStatus as Z, WhatsAppService as _, TemplateComponentDefinition as a, WhatsAppInteractiveButtonsMessage as at, WHATSAPP_MESSAGE_TYPES as b, TemplateParameterDefinition as c, WhatsAppLocationMessage as ct, TemplateRegistry as d, WebhookEntry as et, defineTemplates as f, OutgoingLoggingMetadata as g, serializeTemplateFromRegistry as h, SupportedTemplateParameterType as i, WhatsAppCarouselCard as it, serializeError as j, createLogger as k, TemplateParameterInputMap as l, WhatsAppTemplateMessage as lt, hasConfiguredTemplates as m, createZapClient as n, WebhookPayload as nt, TemplateDefinition as o, WhatsAppInteractiveListMessage as ot, getTemplateNames as p, SendResult as q, EMPTY_TEMPLATE_REGISTRY as r, WebhookValue as rt, TemplateName as s, WhatsAppInteractiveMediaCarouselMessage as st, ZapClient as t, WebhookError as tt, TemplateParams as u, WhatsAppTextMessage as ut, MessageLoggerNotifier as v, WhatsAppMessageType as w, WhatsAppDirection as x, MessageLoggerService as y, IncomingMessage as z };
789
+ export { UIMessage as $, createLogger as A, ConversationRecord as B, WhatsAppLogRecord as C, LogLevel as D, WhatsAppStatus as E, NewMessageEvent as F, MessageError as G, IncomingMessage as H, StatusUpdateEvent as I, SendMessageError as J, MessageStatus as K, SyncEvent as L, serializeError as M, ConversationSummary as N, Logger as O, ConversationUpdateEvent as P, TemplateParameter as Q, WhatsAppConfig as R, WhatsAppDirection as S, WhatsAppMessageType as T, InteractiveMediaCarouselCardInput as U, FreeformMessageWindow as V, MediaMessage as W, SendResult as X, SendMessageResponse as Y, TemplateComponent as Z, OutgoingLoggingMetadata as _, SupportedTemplateParameterType as a, WebhookPayload as at, MessageLoggerService as b, TemplateName as c, WhatsAppInteractiveButtonsMessage as ct, TemplateParams as d, WhatsAppLocationMessage as dt, UIMessageStatus as et, TemplateRegistry as f, WhatsAppTemplateMessage as ft, serializeTemplateFromRegistry as g, hasConfiguredTemplates as h, EMPTY_TEMPLATE_REGISTRY as i, WebhookError as it, noopLogger as j, LoggerConfig as k, TemplateParameterDefinition as l, WhatsAppInteractiveListMessage as lt, getTemplateNames as m, ZapClient as n, WebhookContact as nt, TemplateComponentDefinition as o, WebhookValue as ot, defineTemplates as p, WhatsAppTextMessage as pt, SendInteractiveMediaCarouselData as q, createZapClient as r, WebhookEntry as rt, TemplateDefinition as s, WhatsAppCarouselCard as st, BetterZapClientError as t, WebhookChange as tt, TemplateParameterInputMap as u, WhatsAppInteractiveMediaCarouselMessage as ut, WhatsAppService as v, WhatsAppLogStore as w, WHATSAPP_MESSAGE_TYPES as x, MessageLoggerNotifier as y, Conversation as z };
package/dist/client.cjs CHANGED
@@ -1,16 +1,36 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  //#region src/client.ts
3
+ var BetterZapClientError = class extends Error {
4
+ status;
5
+ code;
6
+ details;
7
+ body;
8
+ constructor(message, options) {
9
+ super(message);
10
+ this.name = "BetterZapClientError";
11
+ this.status = options.status;
12
+ this.code = options.code;
13
+ this.details = options.details;
14
+ this.body = options.body;
15
+ }
16
+ };
3
17
  function createZapClient(options) {
4
18
  const baseURL = options?.baseURL ?? (typeof window !== "undefined" ? window.location.origin : "");
5
19
  const basePath = options?.basePath ?? "/api/whatsapp";
6
20
  const fetchFn = options?.fetch ?? fetch;
7
21
  async function request(path, init) {
8
22
  const response = await fetchFn(`${baseURL}${basePath}${path}`, init);
23
+ const payload = await response.json().catch(() => null);
9
24
  if (!response.ok) {
10
- const error = await response.json().catch(() => ({}));
11
- throw new Error(error.error || `Request failed: ${response.status}`);
25
+ const error = payload;
26
+ throw new BetterZapClientError(error?.error || `Request failed: ${response.status}`, {
27
+ status: response.status,
28
+ code: error?.code,
29
+ details: error?.details,
30
+ body: payload
31
+ });
12
32
  }
13
- return response.json();
33
+ return payload;
14
34
  }
15
35
  function post(path, body) {
16
36
  return request(path, {
@@ -34,6 +54,7 @@ function createZapClient(options) {
34
54
  conversations: {
35
55
  list: () => request("/conversations"),
36
56
  get: (phone) => request(`/conversations/${normalizePhone(phone)}`).catch((err) => {
57
+ if (err instanceof BetterZapClientError && err.status === 404) return null;
37
58
  if (err.message?.includes("Conversation not found")) return null;
38
59
  throw err;
39
60
  }),
@@ -48,4 +69,5 @@ function createZapClient(options) {
48
69
  };
49
70
  }
50
71
  //#endregion
72
+ exports.BetterZapClientError = BetterZapClientError;
51
73
  exports.createZapClient = createZapClient;
package/dist/client.d.cts CHANGED
@@ -1,2 +1,2 @@
1
- import { n as createZapClient, t as ZapClient } from "./client-D5Lgtacj.cjs";
2
- export { ZapClient, createZapClient };
1
+ import { n as ZapClient, r as createZapClient, t as BetterZapClientError } from "./client-B_VYCUHu.cjs";
2
+ export { BetterZapClientError, ZapClient, createZapClient };
package/dist/client.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { n as createZapClient, t as ZapClient } from "./client-ColqW3Zc.mjs";
2
- export { ZapClient, createZapClient };
1
+ import { n as ZapClient, r as createZapClient, t as BetterZapClientError } from "./client-D-wgralM.mjs";
2
+ export { BetterZapClientError, ZapClient, createZapClient };
package/dist/client.mjs CHANGED
@@ -1,15 +1,35 @@
1
1
  //#region src/client.ts
2
+ var BetterZapClientError = class extends Error {
3
+ status;
4
+ code;
5
+ details;
6
+ body;
7
+ constructor(message, options) {
8
+ super(message);
9
+ this.name = "BetterZapClientError";
10
+ this.status = options.status;
11
+ this.code = options.code;
12
+ this.details = options.details;
13
+ this.body = options.body;
14
+ }
15
+ };
2
16
  function createZapClient(options) {
3
17
  const baseURL = options?.baseURL ?? (typeof window !== "undefined" ? window.location.origin : "");
4
18
  const basePath = options?.basePath ?? "/api/whatsapp";
5
19
  const fetchFn = options?.fetch ?? fetch;
6
20
  async function request(path, init) {
7
21
  const response = await fetchFn(`${baseURL}${basePath}${path}`, init);
22
+ const payload = await response.json().catch(() => null);
8
23
  if (!response.ok) {
9
- const error = await response.json().catch(() => ({}));
10
- throw new Error(error.error || `Request failed: ${response.status}`);
24
+ const error = payload;
25
+ throw new BetterZapClientError(error?.error || `Request failed: ${response.status}`, {
26
+ status: response.status,
27
+ code: error?.code,
28
+ details: error?.details,
29
+ body: payload
30
+ });
11
31
  }
12
- return response.json();
32
+ return payload;
13
33
  }
14
34
  function post(path, body) {
15
35
  return request(path, {
@@ -33,6 +53,7 @@ function createZapClient(options) {
33
53
  conversations: {
34
54
  list: () => request("/conversations"),
35
55
  get: (phone) => request(`/conversations/${normalizePhone(phone)}`).catch((err) => {
56
+ if (err instanceof BetterZapClientError && err.status === 404) return null;
36
57
  if (err.message?.includes("Conversation not found")) return null;
37
58
  throw err;
38
59
  }),
@@ -47,4 +68,4 @@ function createZapClient(options) {
47
68
  };
48
69
  }
49
70
  //#endregion
50
- export { createZapClient };
71
+ export { BetterZapClientError, createZapClient };
package/dist/index.cjs CHANGED
@@ -1,5 +1,61 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_client = require("./client.cjs");
3
+ //#region src/freeform-message-window.ts
4
+ const FREEFORM_MESSAGE_WINDOW_MS = 1440 * 60 * 1e3;
5
+ function toTimestamp(value) {
6
+ if (!value) return null;
7
+ const timestamp = new Date(value).getTime();
8
+ return Number.isFinite(timestamp) ? timestamp : null;
9
+ }
10
+ function createFreeformMessageWindow(lastIncomingMessageAt, now = /* @__PURE__ */ new Date()) {
11
+ const lastIncomingTimestamp = toTimestamp(lastIncomingMessageAt);
12
+ if (lastIncomingTimestamp == null) return {
13
+ isOpen: false,
14
+ lastIncomingMessageAt: null,
15
+ expiresAt: null
16
+ };
17
+ const expiresAtTimestamp = lastIncomingTimestamp + FREEFORM_MESSAGE_WINDOW_MS;
18
+ return {
19
+ isOpen: now.getTime() < expiresAtTimestamp,
20
+ lastIncomingMessageAt,
21
+ expiresAt: new Date(expiresAtTimestamp).toISOString()
22
+ };
23
+ }
24
+ function normalizeConversationRecord(record, now = /* @__PURE__ */ new Date()) {
25
+ const freeformMessageWindow = createFreeformMessageWindow(record.lastIncomingMessageAt, now);
26
+ return {
27
+ ...record,
28
+ lastIncomingMessageAt: freeformMessageWindow.lastIncomingMessageAt,
29
+ freeformMessageWindow
30
+ };
31
+ }
32
+ function normalizeConversationRecords(records, now = /* @__PURE__ */ new Date()) {
33
+ return records.map((record) => normalizeConversationRecord(record, now));
34
+ }
35
+ function getLatestIncomingMessageAt(messages) {
36
+ if (!messages?.length) return null;
37
+ let latestIncomingMessageAt = null;
38
+ let latestTimestamp = -Infinity;
39
+ for (const message of messages) {
40
+ if (message.direction !== "incoming") continue;
41
+ const timestamp = toTimestamp(message.sentAt);
42
+ if (timestamp == null || timestamp <= latestTimestamp) continue;
43
+ latestIncomingMessageAt = message.sentAt;
44
+ latestTimestamp = timestamp;
45
+ }
46
+ return latestIncomingMessageAt;
47
+ }
48
+ function resolveConversationFreeformMessageWindow(conversation, messages, now = /* @__PURE__ */ new Date()) {
49
+ const baseLastIncomingMessageAt = conversation?.freeformMessageWindow?.lastIncomingMessageAt ?? conversation?.lastIncomingMessageAt ?? null;
50
+ const latestIncomingMessageAt = getLatestIncomingMessageAt(messages);
51
+ if (!latestIncomingMessageAt) return createFreeformMessageWindow(baseLastIncomingMessageAt, now);
52
+ const baseTimestamp = toTimestamp(baseLastIncomingMessageAt);
53
+ const latestTimestamp = toTimestamp(latestIncomingMessageAt);
54
+ if (latestTimestamp == null) return createFreeformMessageWindow(baseLastIncomingMessageAt, now);
55
+ if (baseTimestamp != null && latestTimestamp <= baseTimestamp) return createFreeformMessageWindow(baseLastIncomingMessageAt, now);
56
+ return createFreeformMessageWindow(latestIncomingMessageAt, now);
57
+ }
58
+ //#endregion
3
59
  //#region src/logger.ts
4
60
  const LOG_LEVEL_ORDER = {
5
61
  debug: 0,
@@ -78,6 +134,7 @@ function delay(ms) {
78
134
  //#region src/services/whatsapp.service.ts
79
135
  const META_API_VERSION = "v25.0";
80
136
  const META_BASE_URL = "https://graph.facebook.com";
137
+ const CONTEXT_WINDOW_CLOSED_ERROR = "Free-form message window is closed.";
81
138
  var WhatsAppService = class {
82
139
  baseUrl;
83
140
  token;
@@ -91,15 +148,31 @@ var WhatsAppService = class {
91
148
  this.logger = logger;
92
149
  this.log = log;
93
150
  }
94
- /** Send a text message (within 24h service window only). */
151
+ /** Send a text message within the 24h free-form message window only. */
95
152
  async sendText(to, body, logging) {
96
- const hasUrl = /https?:\/\/\S+/i.test(body);
153
+ const normalizedPhone = formatPhone(to);
154
+ const freeformMessageWindow = await this.logger.getFreeformMessageWindow(normalizedPhone);
155
+ if (!freeformMessageWindow.isOpen) {
156
+ const result = {
157
+ success: false,
158
+ error: CONTEXT_WINDOW_CLOSED_ERROR,
159
+ code: "CONTEXT_WINDOW_CLOSED",
160
+ httpStatus: 409,
161
+ details: { freeformMessageWindow }
162
+ };
163
+ await this.logSendResult(normalizedPhone, logging ? {
164
+ ...logging,
165
+ messageType: logging.messageType || "bot_reply",
166
+ content: body
167
+ } : void 0, result);
168
+ return result;
169
+ }
97
170
  const payload = {
98
171
  messaging_product: "whatsapp",
99
172
  recipient_type: "individual",
100
- to: formatPhone(to),
173
+ to: normalizedPhone,
101
174
  type: "text",
102
- text: hasUrl ? {
175
+ text: /https?:\/\/\S+/i.test(body) ? {
103
176
  body,
104
177
  preview_url: true
105
178
  } : { body }
@@ -311,20 +384,24 @@ var WhatsAppService = class {
311
384
  };
312
385
  }
313
386
  const result = await this.performRequest(payload, retries);
314
- if (logging) try {
387
+ await this.logSendResult(payload.to, logging, result, payload.type === "template" ? payload.template.name : void 0);
388
+ return result;
389
+ }
390
+ async logSendResult(phone, logging, result, templateName) {
391
+ if (!logging) return;
392
+ try {
315
393
  await this.logger.logOutgoing({
316
- phone: payload.to,
394
+ phone,
317
395
  userId: logging.userId,
318
396
  messageType: logging.messageType,
319
397
  content: logging.content,
320
- templateName: payload.type === "template" ? payload.template.name : void 0,
398
+ templateName,
321
399
  result,
322
400
  metadata: logging.metadata
323
401
  });
324
402
  } catch (logError) {
325
403
  this.log.error("whatsapp.log_failed", serializeError(logError));
326
404
  }
327
- return result;
328
405
  }
329
406
  /** Actually performs the network request with retries. */
330
407
  async performRequest(payload, retries) {
@@ -406,6 +483,24 @@ var MessageLoggerService = class {
406
483
  this.log.error("message_logger.sync_notify_failed", serializeError(err));
407
484
  }
408
485
  }
486
+ async getConversationById(conversationId) {
487
+ const conversation = await this.store.getConversationById(conversationId);
488
+ return conversation ? normalizeConversationRecord(conversation) : null;
489
+ }
490
+ async getConversationByPhone(phone) {
491
+ const conversation = await this.store.getConversationByPhone(phone);
492
+ return conversation ? normalizeConversationRecord(conversation) : null;
493
+ }
494
+ async getConversations() {
495
+ return normalizeConversationRecords(await this.store.getConversations());
496
+ }
497
+ /** @deprecated Prefer `getFreeformMessageWindow()`. */
498
+ async getCustomerCareWindow(phone) {
499
+ return this.getFreeformMessageWindow(phone);
500
+ }
501
+ async getFreeformMessageWindow(phone) {
502
+ return (await this.getConversationByPhone(phone))?.freeformMessageWindow ?? createFreeformMessageWindow(null);
503
+ }
409
504
  /**
410
505
  * Check if a message with this waMessageId was already processed.
411
506
  */
@@ -429,7 +524,7 @@ var MessageLoggerService = class {
429
524
  sentAt: (/* @__PURE__ */ new Date()).toISOString(),
430
525
  metadata: params.metadata
431
526
  });
432
- const conversation = await this.store.getConversationById(inserted.conversationId);
527
+ const conversation = await this.getConversationById(inserted.conversationId);
433
528
  if (conversation) await this.notify({
434
529
  type: "NEW_MESSAGE",
435
530
  message: inserted,
@@ -481,9 +576,9 @@ var MessageLoggerService = class {
481
576
  content: params.content,
482
577
  status: "delivered",
483
578
  metadata: params.metadata,
484
- sentAt: (/* @__PURE__ */ new Date()).toISOString()
579
+ sentAt: params.sentAt
485
580
  });
486
- const conversation = await this.store.getConversationById(inserted.conversationId);
581
+ const conversation = await this.getConversationById(inserted.conversationId);
487
582
  if (conversation) await this.notify({
488
583
  type: "NEW_MESSAGE",
489
584
  message: inserted,
@@ -564,17 +659,24 @@ function serializeTemplateParameter(parameter, value) {
564
659
  }
565
660
  }
566
661
  //#endregion
662
+ exports.BetterZapClientError = require_client.BetterZapClientError;
567
663
  exports.EMPTY_TEMPLATE_REGISTRY = EMPTY_TEMPLATE_REGISTRY;
664
+ exports.FREEFORM_MESSAGE_WINDOW_MS = FREEFORM_MESSAGE_WINDOW_MS;
568
665
  exports.MessageLoggerService = MessageLoggerService;
569
666
  exports.WHATSAPP_MESSAGE_TYPES = WHATSAPP_MESSAGE_TYPES;
570
667
  exports.WhatsAppService = WhatsAppService;
668
+ exports.createFreeformMessageWindow = createFreeformMessageWindow;
571
669
  exports.createLogger = createLogger;
572
670
  exports.createZapClient = require_client.createZapClient;
573
671
  exports.defineTemplates = defineTemplates;
574
672
  exports.delay = delay;
575
673
  exports.formatPhone = formatPhone;
674
+ exports.getLatestIncomingMessageAt = getLatestIncomingMessageAt;
576
675
  exports.getTemplateNames = getTemplateNames;
577
676
  exports.hasConfiguredTemplates = hasConfiguredTemplates;
578
677
  exports.noopLogger = noopLogger;
678
+ exports.normalizeConversationRecord = normalizeConversationRecord;
679
+ exports.normalizeConversationRecords = normalizeConversationRecords;
680
+ exports.resolveConversationFreeformMessageWindow = resolveConversationFreeformMessageWindow;
579
681
  exports.serializeError = serializeError;
580
682
  exports.serializeTemplateFromRegistry = serializeTemplateFromRegistry;
package/dist/index.d.cts CHANGED
@@ -1,5 +1,13 @@
1
- import { $ as WebhookContact, A as noopLogger, B as InteractiveMediaCarouselCardInput, C as WhatsAppLogStore, D as Logger, E as LogLevel, F as StatusUpdateEvent, G as SendMessageError, H as MessageError, I as SyncEvent, J as TemplateComponent, K as SendMessageResponse, L as WhatsAppConfig, M as ConversationSummary, N as ConversationUpdateEvent, O as LoggerConfig, P as NewMessageEvent, Q as WebhookChange, R as Conversation, S as WhatsAppLogRecord, T as WhatsAppStatus, U as MessageStatus, V as MediaMessage, W as SendInteractiveMediaCarouselData, X as UIMessage, Y as TemplateParameter, Z as UIMessageStatus, _ as WhatsAppService, a as TemplateComponentDefinition, at as WhatsAppInteractiveButtonsMessage, b as WHATSAPP_MESSAGE_TYPES, c as TemplateParameterDefinition, ct as WhatsAppLocationMessage, d as TemplateRegistry, et as WebhookEntry, f as defineTemplates, g as OutgoingLoggingMetadata, h as serializeTemplateFromRegistry, i as SupportedTemplateParameterType, it as WhatsAppCarouselCard, j as serializeError, k as createLogger, l as TemplateParameterInputMap, lt as WhatsAppTemplateMessage, m as hasConfiguredTemplates, n as createZapClient, nt as WebhookPayload, o as TemplateDefinition, ot as WhatsAppInteractiveListMessage, p as getTemplateNames, q as SendResult, r as EMPTY_TEMPLATE_REGISTRY, rt as WebhookValue, s as TemplateName, st as WhatsAppInteractiveMediaCarouselMessage, t as ZapClient, tt as WebhookError, u as TemplateParams, ut as WhatsAppTextMessage, v as MessageLoggerNotifier, w as WhatsAppMessageType, x as WhatsAppDirection, y as MessageLoggerService, z as IncomingMessage } from "./client-D5Lgtacj.cjs";
1
+ import { $ as UIMessage, A as createLogger, B as ConversationRecord, C as WhatsAppLogRecord, D as LogLevel, E as WhatsAppStatus, F as NewMessageEvent, G as MessageError, H as IncomingMessage, I as StatusUpdateEvent, J as SendMessageError, K as MessageStatus, L as SyncEvent, M as serializeError, N as ConversationSummary, O as Logger, P as ConversationUpdateEvent, Q as TemplateParameter, R as WhatsAppConfig, S as WhatsAppDirection, T as WhatsAppMessageType, U as InteractiveMediaCarouselCardInput, V as FreeformMessageWindow, W as MediaMessage, X as SendResult, Y as SendMessageResponse, Z as TemplateComponent, _ as OutgoingLoggingMetadata, a as SupportedTemplateParameterType, at as WebhookPayload, b as MessageLoggerService, c as TemplateName, ct as WhatsAppInteractiveButtonsMessage, d as TemplateParams, dt as WhatsAppLocationMessage, et as UIMessageStatus, f as TemplateRegistry, ft as WhatsAppTemplateMessage, g as serializeTemplateFromRegistry, h as hasConfiguredTemplates, i as EMPTY_TEMPLATE_REGISTRY, it as WebhookError, j as noopLogger, k as LoggerConfig, l as TemplateParameterDefinition, lt as WhatsAppInteractiveListMessage, m as getTemplateNames, n as ZapClient, nt as WebhookContact, o as TemplateComponentDefinition, ot as WebhookValue, p as defineTemplates, pt as WhatsAppTextMessage, q as SendInteractiveMediaCarouselData, r as createZapClient, rt as WebhookEntry, s as TemplateDefinition, st as WhatsAppCarouselCard, t as BetterZapClientError, tt as WebhookChange, u as TemplateParameterInputMap, ut as WhatsAppInteractiveMediaCarouselMessage, v as WhatsAppService, w as WhatsAppLogStore, x as WHATSAPP_MESSAGE_TYPES, y as MessageLoggerNotifier, z as Conversation } from "./client-B_VYCUHu.cjs";
2
2
 
3
+ //#region src/freeform-message-window.d.ts
4
+ declare const FREEFORM_MESSAGE_WINDOW_MS: number;
5
+ declare function createFreeformMessageWindow(lastIncomingMessageAt: string | null, now?: Date): FreeformMessageWindow;
6
+ declare function normalizeConversationRecord(record: ConversationRecord, now?: Date): Conversation;
7
+ declare function normalizeConversationRecords(records: ConversationRecord[], now?: Date): Conversation[];
8
+ declare function getLatestIncomingMessageAt(messages: UIMessage[] | undefined): string | null;
9
+ declare function resolveConversationFreeformMessageWindow(conversation: Pick<Conversation, "freeformMessageWindow" | "lastIncomingMessageAt"> | null | undefined, messages?: UIMessage[], now?: Date): FreeformMessageWindow;
10
+ //#endregion
3
11
  //#region src/events.d.ts
4
12
  type MessageContext = {
5
13
  message: IncomingMessage;
@@ -120,4 +128,4 @@ interface BetterZapApi<TTemplates extends TemplateRegistry = {}> {
120
128
  };
121
129
  }
122
130
  //#endregion
123
- export { type Awaitable, type BetterZapApi, type BetterZapContext, type BetterZapCoreConfig, type BetterZapCoreContext, type BetterZapCoreServices, type BetterZapDatabase, type BetterZapPlugin, type BetterZapPluginInitContext, type BetterZapPluginInitResult, type BetterZapServices, type Conversation, type ConversationSummary, type ConversationUpdateEvent, EMPTY_TEMPLATE_REGISTRY, type IncomingMessage, type InferBetterZapPluginContext, type InferBetterZapPluginServices, type InteractiveMediaCarouselCardInput, type LogLevel, type Logger, type LoggerConfig, type MediaMessage, type MessageContext, type MessageError, type MessageLoggerNotifier, MessageLoggerService, type MessageStatus, type NewMessageEvent, type OutgoingLoggingMetadata, type SendInteractiveMediaCarouselData, type SendMessageError, type SendMessageResponse, type SendResult, type StatusContext, type StatusUpdateEvent, type SupportedTemplateParameterType, type SyncEvent, type TemplateComponent, type TemplateComponentDefinition, type TemplateDefinition, type TemplateName, type TemplateParameter, type TemplateParameterDefinition, type TemplateParameterInputMap, type TemplateParams, type TemplateRegistry, type UIMessage, type UIMessageStatus, WHATSAPP_MESSAGE_TYPES, type WebhookChange, type WebhookContact, type WebhookEntry, type WebhookError, type WebhookPayload, type WebhookValue, type WhatsAppCarouselCard, type WhatsAppConfig, type WhatsAppDirection, type WhatsAppInteractiveButtonsMessage, type WhatsAppInteractiveListMessage, type WhatsAppInteractiveMediaCarouselMessage, type WhatsAppLocationMessage, type WhatsAppLogRecord, type WhatsAppLogStore, type WhatsAppMessageType, WhatsAppService, type WhatsAppStatus, type WhatsAppTemplateMessage, type WhatsAppTextMessage, type ZapClient, createLogger, createZapClient, defineTemplates, delay, formatPhone, getTemplateNames, hasConfiguredTemplates, noopLogger, serializeError, serializeTemplateFromRegistry };
131
+ export { type Awaitable, type BetterZapApi, BetterZapClientError, type BetterZapContext, type BetterZapCoreConfig, type BetterZapCoreContext, type BetterZapCoreServices, type BetterZapDatabase, type BetterZapPlugin, type BetterZapPluginInitContext, type BetterZapPluginInitResult, type BetterZapServices, type Conversation, type ConversationRecord, type ConversationSummary, type ConversationUpdateEvent, EMPTY_TEMPLATE_REGISTRY, FREEFORM_MESSAGE_WINDOW_MS, type FreeformMessageWindow, type IncomingMessage, type InferBetterZapPluginContext, type InferBetterZapPluginServices, type InteractiveMediaCarouselCardInput, type LogLevel, type Logger, type LoggerConfig, type MediaMessage, type MessageContext, type MessageError, type MessageLoggerNotifier, MessageLoggerService, type MessageStatus, type NewMessageEvent, type OutgoingLoggingMetadata, type SendInteractiveMediaCarouselData, type SendMessageError, type SendMessageResponse, type SendResult, type StatusContext, type StatusUpdateEvent, type SupportedTemplateParameterType, type SyncEvent, type TemplateComponent, type TemplateComponentDefinition, type TemplateDefinition, type TemplateName, type TemplateParameter, type TemplateParameterDefinition, type TemplateParameterInputMap, type TemplateParams, type TemplateRegistry, type UIMessage, type UIMessageStatus, WHATSAPP_MESSAGE_TYPES, type WebhookChange, type WebhookContact, type WebhookEntry, type WebhookError, type WebhookPayload, type WebhookValue, type WhatsAppCarouselCard, type WhatsAppConfig, type WhatsAppDirection, type WhatsAppInteractiveButtonsMessage, type WhatsAppInteractiveListMessage, type WhatsAppInteractiveMediaCarouselMessage, type WhatsAppLocationMessage, type WhatsAppLogRecord, type WhatsAppLogStore, type WhatsAppMessageType, WhatsAppService, type WhatsAppStatus, type WhatsAppTemplateMessage, type WhatsAppTextMessage, type ZapClient, createFreeformMessageWindow, createLogger, createZapClient, defineTemplates, delay, formatPhone, getLatestIncomingMessageAt, getTemplateNames, hasConfiguredTemplates, noopLogger, normalizeConversationRecord, normalizeConversationRecords, resolveConversationFreeformMessageWindow, serializeError, serializeTemplateFromRegistry };
package/dist/index.d.mts CHANGED
@@ -1,5 +1,13 @@
1
- import { $ as WebhookContact, A as noopLogger, B as InteractiveMediaCarouselCardInput, C as WhatsAppLogStore, D as Logger, E as LogLevel, F as StatusUpdateEvent, G as SendMessageError, H as MessageError, I as SyncEvent, J as TemplateComponent, K as SendMessageResponse, L as WhatsAppConfig, M as ConversationSummary, N as ConversationUpdateEvent, O as LoggerConfig, P as NewMessageEvent, Q as WebhookChange, R as Conversation, S as WhatsAppLogRecord, T as WhatsAppStatus, U as MessageStatus, V as MediaMessage, W as SendInteractiveMediaCarouselData, X as UIMessage, Y as TemplateParameter, Z as UIMessageStatus, _ as WhatsAppService, a as TemplateComponentDefinition, at as WhatsAppInteractiveButtonsMessage, b as WHATSAPP_MESSAGE_TYPES, c as TemplateParameterDefinition, ct as WhatsAppLocationMessage, d as TemplateRegistry, et as WebhookEntry, f as defineTemplates, g as OutgoingLoggingMetadata, h as serializeTemplateFromRegistry, i as SupportedTemplateParameterType, it as WhatsAppCarouselCard, j as serializeError, k as createLogger, l as TemplateParameterInputMap, lt as WhatsAppTemplateMessage, m as hasConfiguredTemplates, n as createZapClient, nt as WebhookPayload, o as TemplateDefinition, ot as WhatsAppInteractiveListMessage, p as getTemplateNames, q as SendResult, r as EMPTY_TEMPLATE_REGISTRY, rt as WebhookValue, s as TemplateName, st as WhatsAppInteractiveMediaCarouselMessage, t as ZapClient, tt as WebhookError, u as TemplateParams, ut as WhatsAppTextMessage, v as MessageLoggerNotifier, w as WhatsAppMessageType, x as WhatsAppDirection, y as MessageLoggerService, z as IncomingMessage } from "./client-ColqW3Zc.mjs";
1
+ import { $ as UIMessage, A as createLogger, B as ConversationRecord, C as WhatsAppLogRecord, D as LogLevel, E as WhatsAppStatus, F as NewMessageEvent, G as MessageError, H as IncomingMessage, I as StatusUpdateEvent, J as SendMessageError, K as MessageStatus, L as SyncEvent, M as serializeError, N as ConversationSummary, O as Logger, P as ConversationUpdateEvent, Q as TemplateParameter, R as WhatsAppConfig, S as WhatsAppDirection, T as WhatsAppMessageType, U as InteractiveMediaCarouselCardInput, V as FreeformMessageWindow, W as MediaMessage, X as SendResult, Y as SendMessageResponse, Z as TemplateComponent, _ as OutgoingLoggingMetadata, a as SupportedTemplateParameterType, at as WebhookPayload, b as MessageLoggerService, c as TemplateName, ct as WhatsAppInteractiveButtonsMessage, d as TemplateParams, dt as WhatsAppLocationMessage, et as UIMessageStatus, f as TemplateRegistry, ft as WhatsAppTemplateMessage, g as serializeTemplateFromRegistry, h as hasConfiguredTemplates, i as EMPTY_TEMPLATE_REGISTRY, it as WebhookError, j as noopLogger, k as LoggerConfig, l as TemplateParameterDefinition, lt as WhatsAppInteractiveListMessage, m as getTemplateNames, n as ZapClient, nt as WebhookContact, o as TemplateComponentDefinition, ot as WebhookValue, p as defineTemplates, pt as WhatsAppTextMessage, q as SendInteractiveMediaCarouselData, r as createZapClient, rt as WebhookEntry, s as TemplateDefinition, st as WhatsAppCarouselCard, t as BetterZapClientError, tt as WebhookChange, u as TemplateParameterInputMap, ut as WhatsAppInteractiveMediaCarouselMessage, v as WhatsAppService, w as WhatsAppLogStore, x as WHATSAPP_MESSAGE_TYPES, y as MessageLoggerNotifier, z as Conversation } from "./client-D-wgralM.mjs";
2
2
 
3
+ //#region src/freeform-message-window.d.ts
4
+ declare const FREEFORM_MESSAGE_WINDOW_MS: number;
5
+ declare function createFreeformMessageWindow(lastIncomingMessageAt: string | null, now?: Date): FreeformMessageWindow;
6
+ declare function normalizeConversationRecord(record: ConversationRecord, now?: Date): Conversation;
7
+ declare function normalizeConversationRecords(records: ConversationRecord[], now?: Date): Conversation[];
8
+ declare function getLatestIncomingMessageAt(messages: UIMessage[] | undefined): string | null;
9
+ declare function resolveConversationFreeformMessageWindow(conversation: Pick<Conversation, "freeformMessageWindow" | "lastIncomingMessageAt"> | null | undefined, messages?: UIMessage[], now?: Date): FreeformMessageWindow;
10
+ //#endregion
3
11
  //#region src/events.d.ts
4
12
  type MessageContext = {
5
13
  message: IncomingMessage;
@@ -120,4 +128,4 @@ interface BetterZapApi<TTemplates extends TemplateRegistry = {}> {
120
128
  };
121
129
  }
122
130
  //#endregion
123
- export { type Awaitable, type BetterZapApi, type BetterZapContext, type BetterZapCoreConfig, type BetterZapCoreContext, type BetterZapCoreServices, type BetterZapDatabase, type BetterZapPlugin, type BetterZapPluginInitContext, type BetterZapPluginInitResult, type BetterZapServices, type Conversation, type ConversationSummary, type ConversationUpdateEvent, EMPTY_TEMPLATE_REGISTRY, type IncomingMessage, type InferBetterZapPluginContext, type InferBetterZapPluginServices, type InteractiveMediaCarouselCardInput, type LogLevel, type Logger, type LoggerConfig, type MediaMessage, type MessageContext, type MessageError, type MessageLoggerNotifier, MessageLoggerService, type MessageStatus, type NewMessageEvent, type OutgoingLoggingMetadata, type SendInteractiveMediaCarouselData, type SendMessageError, type SendMessageResponse, type SendResult, type StatusContext, type StatusUpdateEvent, type SupportedTemplateParameterType, type SyncEvent, type TemplateComponent, type TemplateComponentDefinition, type TemplateDefinition, type TemplateName, type TemplateParameter, type TemplateParameterDefinition, type TemplateParameterInputMap, type TemplateParams, type TemplateRegistry, type UIMessage, type UIMessageStatus, WHATSAPP_MESSAGE_TYPES, type WebhookChange, type WebhookContact, type WebhookEntry, type WebhookError, type WebhookPayload, type WebhookValue, type WhatsAppCarouselCard, type WhatsAppConfig, type WhatsAppDirection, type WhatsAppInteractiveButtonsMessage, type WhatsAppInteractiveListMessage, type WhatsAppInteractiveMediaCarouselMessage, type WhatsAppLocationMessage, type WhatsAppLogRecord, type WhatsAppLogStore, type WhatsAppMessageType, WhatsAppService, type WhatsAppStatus, type WhatsAppTemplateMessage, type WhatsAppTextMessage, type ZapClient, createLogger, createZapClient, defineTemplates, delay, formatPhone, getTemplateNames, hasConfiguredTemplates, noopLogger, serializeError, serializeTemplateFromRegistry };
131
+ export { type Awaitable, type BetterZapApi, BetterZapClientError, type BetterZapContext, type BetterZapCoreConfig, type BetterZapCoreContext, type BetterZapCoreServices, type BetterZapDatabase, type BetterZapPlugin, type BetterZapPluginInitContext, type BetterZapPluginInitResult, type BetterZapServices, type Conversation, type ConversationRecord, type ConversationSummary, type ConversationUpdateEvent, EMPTY_TEMPLATE_REGISTRY, FREEFORM_MESSAGE_WINDOW_MS, type FreeformMessageWindow, type IncomingMessage, type InferBetterZapPluginContext, type InferBetterZapPluginServices, type InteractiveMediaCarouselCardInput, type LogLevel, type Logger, type LoggerConfig, type MediaMessage, type MessageContext, type MessageError, type MessageLoggerNotifier, MessageLoggerService, type MessageStatus, type NewMessageEvent, type OutgoingLoggingMetadata, type SendInteractiveMediaCarouselData, type SendMessageError, type SendMessageResponse, type SendResult, type StatusContext, type StatusUpdateEvent, type SupportedTemplateParameterType, type SyncEvent, type TemplateComponent, type TemplateComponentDefinition, type TemplateDefinition, type TemplateName, type TemplateParameter, type TemplateParameterDefinition, type TemplateParameterInputMap, type TemplateParams, type TemplateRegistry, type UIMessage, type UIMessageStatus, WHATSAPP_MESSAGE_TYPES, type WebhookChange, type WebhookContact, type WebhookEntry, type WebhookError, type WebhookPayload, type WebhookValue, type WhatsAppCarouselCard, type WhatsAppConfig, type WhatsAppDirection, type WhatsAppInteractiveButtonsMessage, type WhatsAppInteractiveListMessage, type WhatsAppInteractiveMediaCarouselMessage, type WhatsAppLocationMessage, type WhatsAppLogRecord, type WhatsAppLogStore, type WhatsAppMessageType, WhatsAppService, type WhatsAppStatus, type WhatsAppTemplateMessage, type WhatsAppTextMessage, type ZapClient, createFreeformMessageWindow, createLogger, createZapClient, defineTemplates, delay, formatPhone, getLatestIncomingMessageAt, getTemplateNames, hasConfiguredTemplates, noopLogger, normalizeConversationRecord, normalizeConversationRecords, resolveConversationFreeformMessageWindow, serializeError, serializeTemplateFromRegistry };
package/dist/index.mjs CHANGED
@@ -1,4 +1,60 @@
1
- import { createZapClient } from "./client.mjs";
1
+ import { BetterZapClientError, createZapClient } from "./client.mjs";
2
+ //#region src/freeform-message-window.ts
3
+ const FREEFORM_MESSAGE_WINDOW_MS = 1440 * 60 * 1e3;
4
+ function toTimestamp(value) {
5
+ if (!value) return null;
6
+ const timestamp = new Date(value).getTime();
7
+ return Number.isFinite(timestamp) ? timestamp : null;
8
+ }
9
+ function createFreeformMessageWindow(lastIncomingMessageAt, now = /* @__PURE__ */ new Date()) {
10
+ const lastIncomingTimestamp = toTimestamp(lastIncomingMessageAt);
11
+ if (lastIncomingTimestamp == null) return {
12
+ isOpen: false,
13
+ lastIncomingMessageAt: null,
14
+ expiresAt: null
15
+ };
16
+ const expiresAtTimestamp = lastIncomingTimestamp + FREEFORM_MESSAGE_WINDOW_MS;
17
+ return {
18
+ isOpen: now.getTime() < expiresAtTimestamp,
19
+ lastIncomingMessageAt,
20
+ expiresAt: new Date(expiresAtTimestamp).toISOString()
21
+ };
22
+ }
23
+ function normalizeConversationRecord(record, now = /* @__PURE__ */ new Date()) {
24
+ const freeformMessageWindow = createFreeformMessageWindow(record.lastIncomingMessageAt, now);
25
+ return {
26
+ ...record,
27
+ lastIncomingMessageAt: freeformMessageWindow.lastIncomingMessageAt,
28
+ freeformMessageWindow
29
+ };
30
+ }
31
+ function normalizeConversationRecords(records, now = /* @__PURE__ */ new Date()) {
32
+ return records.map((record) => normalizeConversationRecord(record, now));
33
+ }
34
+ function getLatestIncomingMessageAt(messages) {
35
+ if (!messages?.length) return null;
36
+ let latestIncomingMessageAt = null;
37
+ let latestTimestamp = -Infinity;
38
+ for (const message of messages) {
39
+ if (message.direction !== "incoming") continue;
40
+ const timestamp = toTimestamp(message.sentAt);
41
+ if (timestamp == null || timestamp <= latestTimestamp) continue;
42
+ latestIncomingMessageAt = message.sentAt;
43
+ latestTimestamp = timestamp;
44
+ }
45
+ return latestIncomingMessageAt;
46
+ }
47
+ function resolveConversationFreeformMessageWindow(conversation, messages, now = /* @__PURE__ */ new Date()) {
48
+ const baseLastIncomingMessageAt = conversation?.freeformMessageWindow?.lastIncomingMessageAt ?? conversation?.lastIncomingMessageAt ?? null;
49
+ const latestIncomingMessageAt = getLatestIncomingMessageAt(messages);
50
+ if (!latestIncomingMessageAt) return createFreeformMessageWindow(baseLastIncomingMessageAt, now);
51
+ const baseTimestamp = toTimestamp(baseLastIncomingMessageAt);
52
+ const latestTimestamp = toTimestamp(latestIncomingMessageAt);
53
+ if (latestTimestamp == null) return createFreeformMessageWindow(baseLastIncomingMessageAt, now);
54
+ if (baseTimestamp != null && latestTimestamp <= baseTimestamp) return createFreeformMessageWindow(baseLastIncomingMessageAt, now);
55
+ return createFreeformMessageWindow(latestIncomingMessageAt, now);
56
+ }
57
+ //#endregion
2
58
  //#region src/logger.ts
3
59
  const LOG_LEVEL_ORDER = {
4
60
  debug: 0,
@@ -77,6 +133,7 @@ function delay(ms) {
77
133
  //#region src/services/whatsapp.service.ts
78
134
  const META_API_VERSION = "v25.0";
79
135
  const META_BASE_URL = "https://graph.facebook.com";
136
+ const CONTEXT_WINDOW_CLOSED_ERROR = "Free-form message window is closed.";
80
137
  var WhatsAppService = class {
81
138
  baseUrl;
82
139
  token;
@@ -90,15 +147,31 @@ var WhatsAppService = class {
90
147
  this.logger = logger;
91
148
  this.log = log;
92
149
  }
93
- /** Send a text message (within 24h service window only). */
150
+ /** Send a text message within the 24h free-form message window only. */
94
151
  async sendText(to, body, logging) {
95
- const hasUrl = /https?:\/\/\S+/i.test(body);
152
+ const normalizedPhone = formatPhone(to);
153
+ const freeformMessageWindow = await this.logger.getFreeformMessageWindow(normalizedPhone);
154
+ if (!freeformMessageWindow.isOpen) {
155
+ const result = {
156
+ success: false,
157
+ error: CONTEXT_WINDOW_CLOSED_ERROR,
158
+ code: "CONTEXT_WINDOW_CLOSED",
159
+ httpStatus: 409,
160
+ details: { freeformMessageWindow }
161
+ };
162
+ await this.logSendResult(normalizedPhone, logging ? {
163
+ ...logging,
164
+ messageType: logging.messageType || "bot_reply",
165
+ content: body
166
+ } : void 0, result);
167
+ return result;
168
+ }
96
169
  const payload = {
97
170
  messaging_product: "whatsapp",
98
171
  recipient_type: "individual",
99
- to: formatPhone(to),
172
+ to: normalizedPhone,
100
173
  type: "text",
101
- text: hasUrl ? {
174
+ text: /https?:\/\/\S+/i.test(body) ? {
102
175
  body,
103
176
  preview_url: true
104
177
  } : { body }
@@ -310,20 +383,24 @@ var WhatsAppService = class {
310
383
  };
311
384
  }
312
385
  const result = await this.performRequest(payload, retries);
313
- if (logging) try {
386
+ await this.logSendResult(payload.to, logging, result, payload.type === "template" ? payload.template.name : void 0);
387
+ return result;
388
+ }
389
+ async logSendResult(phone, logging, result, templateName) {
390
+ if (!logging) return;
391
+ try {
314
392
  await this.logger.logOutgoing({
315
- phone: payload.to,
393
+ phone,
316
394
  userId: logging.userId,
317
395
  messageType: logging.messageType,
318
396
  content: logging.content,
319
- templateName: payload.type === "template" ? payload.template.name : void 0,
397
+ templateName,
320
398
  result,
321
399
  metadata: logging.metadata
322
400
  });
323
401
  } catch (logError) {
324
402
  this.log.error("whatsapp.log_failed", serializeError(logError));
325
403
  }
326
- return result;
327
404
  }
328
405
  /** Actually performs the network request with retries. */
329
406
  async performRequest(payload, retries) {
@@ -405,6 +482,24 @@ var MessageLoggerService = class {
405
482
  this.log.error("message_logger.sync_notify_failed", serializeError(err));
406
483
  }
407
484
  }
485
+ async getConversationById(conversationId) {
486
+ const conversation = await this.store.getConversationById(conversationId);
487
+ return conversation ? normalizeConversationRecord(conversation) : null;
488
+ }
489
+ async getConversationByPhone(phone) {
490
+ const conversation = await this.store.getConversationByPhone(phone);
491
+ return conversation ? normalizeConversationRecord(conversation) : null;
492
+ }
493
+ async getConversations() {
494
+ return normalizeConversationRecords(await this.store.getConversations());
495
+ }
496
+ /** @deprecated Prefer `getFreeformMessageWindow()`. */
497
+ async getCustomerCareWindow(phone) {
498
+ return this.getFreeformMessageWindow(phone);
499
+ }
500
+ async getFreeformMessageWindow(phone) {
501
+ return (await this.getConversationByPhone(phone))?.freeformMessageWindow ?? createFreeformMessageWindow(null);
502
+ }
408
503
  /**
409
504
  * Check if a message with this waMessageId was already processed.
410
505
  */
@@ -428,7 +523,7 @@ var MessageLoggerService = class {
428
523
  sentAt: (/* @__PURE__ */ new Date()).toISOString(),
429
524
  metadata: params.metadata
430
525
  });
431
- const conversation = await this.store.getConversationById(inserted.conversationId);
526
+ const conversation = await this.getConversationById(inserted.conversationId);
432
527
  if (conversation) await this.notify({
433
528
  type: "NEW_MESSAGE",
434
529
  message: inserted,
@@ -480,9 +575,9 @@ var MessageLoggerService = class {
480
575
  content: params.content,
481
576
  status: "delivered",
482
577
  metadata: params.metadata,
483
- sentAt: (/* @__PURE__ */ new Date()).toISOString()
578
+ sentAt: params.sentAt
484
579
  });
485
- const conversation = await this.store.getConversationById(inserted.conversationId);
580
+ const conversation = await this.getConversationById(inserted.conversationId);
486
581
  if (conversation) await this.notify({
487
582
  type: "NEW_MESSAGE",
488
583
  message: inserted,
@@ -563,4 +658,4 @@ function serializeTemplateParameter(parameter, value) {
563
658
  }
564
659
  }
565
660
  //#endregion
566
- export { EMPTY_TEMPLATE_REGISTRY, MessageLoggerService, WHATSAPP_MESSAGE_TYPES, WhatsAppService, createLogger, createZapClient, defineTemplates, delay, formatPhone, getTemplateNames, hasConfiguredTemplates, noopLogger, serializeError, serializeTemplateFromRegistry };
661
+ export { BetterZapClientError, EMPTY_TEMPLATE_REGISTRY, FREEFORM_MESSAGE_WINDOW_MS, MessageLoggerService, WHATSAPP_MESSAGE_TYPES, WhatsAppService, createFreeformMessageWindow, createLogger, createZapClient, defineTemplates, delay, formatPhone, getLatestIncomingMessageAt, getTemplateNames, hasConfiguredTemplates, noopLogger, normalizeConversationRecord, normalizeConversationRecords, resolveConversationFreeformMessageWindow, serializeError, serializeTemplateFromRegistry };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "better-zap",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Framework-agnostic Better Zap core for typed WhatsApp integrations.",
5
5
  "license": "ISC",
6
6
  "type": "module",