better-zap 0.0.4 → 0.2.0

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/index.cjs CHANGED
@@ -132,8 +132,8 @@ function delay(ms) {
132
132
  }
133
133
  //#endregion
134
134
  //#region src/services/whatsapp.service.ts
135
- const META_API_VERSION = "v25.0";
136
- const META_BASE_URL = "https://graph.facebook.com";
135
+ const META_API_VERSION$1 = "v25.0";
136
+ const META_BASE_URL$1 = "https://graph.facebook.com";
137
137
  const CONTEXT_WINDOW_CLOSED_ERROR = "Free-form message window is closed.";
138
138
  var WhatsAppService = class {
139
139
  baseUrl;
@@ -142,7 +142,7 @@ var WhatsAppService = class {
142
142
  logger;
143
143
  log;
144
144
  constructor(config, logger, log) {
145
- this.baseUrl = `${META_BASE_URL}/${META_API_VERSION}/${config.phoneId}/messages`;
145
+ this.baseUrl = `${META_BASE_URL$1}/${META_API_VERSION$1}/${config.phoneId}/messages`;
146
146
  this.token = config.token;
147
147
  this.isDev = config.environment === "development";
148
148
  this.logger = logger;
@@ -457,6 +457,366 @@ var WhatsAppService = class {
457
457
  }
458
458
  };
459
459
  //#endregion
460
+ //#region src/services/coexistence.service.ts
461
+ const META_API_VERSION = "v25.0";
462
+ const META_BASE_URL = "https://graph.facebook.com";
463
+ var CoexistenceService = class {
464
+ accessToken;
465
+ appId;
466
+ appSecret;
467
+ graphApiVersion;
468
+ graphBaseUrl;
469
+ fetchImpl;
470
+ tokenProvider;
471
+ credentialProvider;
472
+ constructor(config) {
473
+ this.accessToken = config.accessToken;
474
+ this.appId = config.appId;
475
+ this.appSecret = config.appSecret;
476
+ this.graphApiVersion = config.graphApiVersion ?? META_API_VERSION;
477
+ this.graphBaseUrl = config.graphBaseUrl ?? META_BASE_URL;
478
+ this.fetchImpl = config.fetch ?? fetch;
479
+ this.tokenProvider = config.tokenProvider ?? config.credentialProvider;
480
+ this.credentialProvider = config.credentialProvider;
481
+ }
482
+ async exchangeEmbeddedSignupCode(input) {
483
+ if (this.credentialProvider) try {
484
+ return {
485
+ success: true,
486
+ data: await this.credentialProvider.exchangeEmbeddedSignupCode(input)
487
+ };
488
+ } catch (error) {
489
+ return {
490
+ success: false,
491
+ error: error instanceof Error ? error.message : "Token exchange failed"
492
+ };
493
+ }
494
+ if (!this.appId || !this.appSecret) return {
495
+ success: false,
496
+ error: "appId and appSecret are required when no credentialProvider is configured"
497
+ };
498
+ const params = new URLSearchParams({
499
+ client_id: this.appId,
500
+ client_secret: this.appSecret,
501
+ code: input.code
502
+ });
503
+ if (input.redirectUri) params.set("redirect_uri", input.redirectUri);
504
+ const result = await this.request({
505
+ path: "/oauth/access_token",
506
+ method: "GET",
507
+ query: params,
508
+ token: null
509
+ });
510
+ if (!result.success || !result.data) return {
511
+ success: false,
512
+ error: result.error,
513
+ errorCode: result.errorCode,
514
+ httpStatus: result.httpStatus,
515
+ details: result.details
516
+ };
517
+ return {
518
+ success: true,
519
+ data: {
520
+ accessToken: result.data.access_token,
521
+ tokenType: result.data.token_type,
522
+ expiresIn: result.data.expires_in,
523
+ raw: result.data
524
+ }
525
+ };
526
+ }
527
+ async subscribeWaba(input) {
528
+ return this.request({
529
+ path: `/${input.wabaId}/subscribed_apps`,
530
+ method: "POST",
531
+ token: input.accessToken ?? await this.resolveAccessToken({ wabaId: input.wabaId })
532
+ });
533
+ }
534
+ async getPhoneStatus(input) {
535
+ return this.request({
536
+ path: `/${input.phoneNumberId}`,
537
+ method: "GET",
538
+ query: new URLSearchParams({ fields: "is_on_biz_app,platform_type" }),
539
+ token: input.accessToken ?? await this.resolveAccessToken({ phoneNumberId: input.phoneNumberId })
540
+ });
541
+ }
542
+ async startContactsSync(input) {
543
+ return this.startSmbAppDataSync({
544
+ phoneNumberId: input.phoneNumberId,
545
+ accessToken: input.accessToken,
546
+ syncType: "smb_app_state_sync"
547
+ });
548
+ }
549
+ async startHistorySync(input) {
550
+ return this.startSmbAppDataSync({
551
+ phoneNumberId: input.phoneNumberId,
552
+ accessToken: input.accessToken,
553
+ syncType: "history"
554
+ });
555
+ }
556
+ async startSmbAppDataSync(input) {
557
+ return this.request({
558
+ path: `/${input.phoneNumberId}/smb_app_data`,
559
+ method: "POST",
560
+ body: { sync_type: input.syncType },
561
+ token: input.accessToken ?? await this.resolveAccessToken({ phoneNumberId: input.phoneNumberId })
562
+ });
563
+ }
564
+ async resolveAccessToken(input) {
565
+ if (this.tokenProvider) return this.tokenProvider.getAccessToken(input);
566
+ return this.accessToken;
567
+ }
568
+ async request(input) {
569
+ if (input.token === void 0) return {
570
+ success: false,
571
+ error: "Missing Meta access token"
572
+ };
573
+ const url = new URL(`${this.graphBaseUrl}/${this.graphApiVersion}${input.path}`);
574
+ if (input.query) input.query.forEach((value, key) => url.searchParams.set(key, value));
575
+ try {
576
+ const response = await this.fetchImpl(url.toString(), {
577
+ method: input.method,
578
+ headers: {
579
+ "Content-Type": "application/json",
580
+ ...input.token ? { Authorization: `Bearer ${input.token}` } : {}
581
+ },
582
+ ...input.body ? { body: JSON.stringify(input.body) } : {}
583
+ });
584
+ let data = null;
585
+ try {
586
+ data = await response.json();
587
+ } catch {
588
+ data = null;
589
+ }
590
+ if (!response.ok) {
591
+ const errorData = data;
592
+ return {
593
+ success: false,
594
+ error: errorData?.error?.message ?? `HTTP ${response.status}`,
595
+ errorCode: errorData?.error?.code,
596
+ httpStatus: response.status,
597
+ details: data
598
+ };
599
+ }
600
+ return {
601
+ success: true,
602
+ data
603
+ };
604
+ } catch (error) {
605
+ return {
606
+ success: false,
607
+ error: error instanceof Error ? error.message : "Network error"
608
+ };
609
+ }
610
+ }
611
+ };
612
+ //#endregion
613
+ //#region src/coexistence/config.ts
614
+ function createCoexistenceEmbeddedSignupConfig(input) {
615
+ return {
616
+ config_id: input.configId,
617
+ response_type: "code",
618
+ override_default_response_type: true,
619
+ extras: {
620
+ ...input.setup ? { setup: input.setup } : {},
621
+ featureType: "whatsapp_business_app_onboarding",
622
+ sessionInfoVersion: "3"
623
+ }
624
+ };
625
+ }
626
+ //#endregion
627
+ //#region src/coexistence/events.ts
628
+ const LEGACY_EVENT_BY_GENERIC = {
629
+ FINISH: "FINISH_WHATSAPP_BUSINESS_APP_ONBOARDING",
630
+ CANCEL: "CANCEL_WHATSAPP_BUSINESS_APP_ONBOARDING",
631
+ ERROR: "ERROR_WHATSAPP_BUSINESS_APP_ONBOARDING"
632
+ };
633
+ const GENERIC_EVENT_BY_LEGACY = {
634
+ FINISH_WHATSAPP_BUSINESS_APP_ONBOARDING: "FINISH",
635
+ CANCEL_WHATSAPP_BUSINESS_APP_ONBOARDING: "CANCEL",
636
+ ERROR_WHATSAPP_BUSINESS_APP_ONBOARDING: "ERROR"
637
+ };
638
+ function isKnownGenericEvent(event) {
639
+ return event === "FINISH" || event === "CANCEL" || event === "ERROR";
640
+ }
641
+ function normalizeCoexistenceSessionEvent(event) {
642
+ if (isKnownGenericEvent(event)) return event;
643
+ if (event in GENERIC_EVENT_BY_LEGACY) return GENERIC_EVENT_BY_LEGACY[event];
644
+ return "PROGRESS";
645
+ }
646
+ function toLegacyCoexistenceSessionEvent(event) {
647
+ return isKnownGenericEvent(event) ? LEGACY_EVENT_BY_GENERIC[event] : event;
648
+ }
649
+ function normalizeCoexistenceSessionPayload(payload) {
650
+ return {
651
+ ...payload,
652
+ normalizedEvent: normalizeCoexistenceSessionEvent(payload.event)
653
+ };
654
+ }
655
+ //#endregion
656
+ //#region src/coexistence/embedded-signup.ts
657
+ const DEFAULT_ALLOWED_ORIGINS = ["https://www.facebook.com", "https://web.facebook.com"];
658
+ function parseEmbeddedSignupMessage(data) {
659
+ const payload = typeof data === "string" ? parseJson(data) : data;
660
+ if (typeof payload !== "object" || payload === null) return null;
661
+ const candidate = payload;
662
+ if (candidate.type !== "WA_EMBEDDED_SIGNUP" || candidate.version !== 3 || typeof candidate.event !== "string") return null;
663
+ return {
664
+ event: candidate.event,
665
+ data: typeof candidate.data === "object" && candidate.data !== null ? candidate.data : void 0
666
+ };
667
+ }
668
+ function parseJson(value) {
669
+ try {
670
+ return JSON.parse(value);
671
+ } catch {
672
+ return null;
673
+ }
674
+ }
675
+ function launchCoexistenceEmbeddedSignup(input) {
676
+ const allowedOrigins = new Set([
677
+ ...DEFAULT_ALLOWED_ORIGINS,
678
+ ...input.origin ? [input.origin] : [],
679
+ ...input.allowedOrigins ?? []
680
+ ]);
681
+ let latestSession = null;
682
+ let latestCode = null;
683
+ const listener = (event) => {
684
+ if (!allowedOrigins.has(event.origin)) return;
685
+ const session = parseEmbeddedSignupMessage(event.data);
686
+ if (!session) return;
687
+ latestSession = session;
688
+ const normalizedEvent = normalizeCoexistenceSessionEvent(session.event);
689
+ if (normalizedEvent === "FINISH") {
690
+ input.onFinish?.({
691
+ code: latestCode,
692
+ session
693
+ });
694
+ return;
695
+ }
696
+ if (normalizedEvent === "CANCEL") {
697
+ input.onCancel?.({ session });
698
+ return;
699
+ }
700
+ if (normalizedEvent === "ERROR") {
701
+ input.onError?.({ session });
702
+ return;
703
+ }
704
+ input.onProgress?.({ session });
705
+ };
706
+ input.target.addEventListener("message", listener);
707
+ input.fb.init?.(input.fbInit ?? {});
708
+ return {
709
+ result: new Promise((resolve) => {
710
+ input.fb.login((response) => {
711
+ latestCode = typeof response.authResponse?.code === "string" ? response.authResponse.code : null;
712
+ if (latestSession && normalizeCoexistenceSessionEvent(latestSession.event) === "FINISH") input.onFinish?.({
713
+ code: latestCode,
714
+ session: latestSession
715
+ });
716
+ resolve({
717
+ code: latestCode,
718
+ session: latestSession,
719
+ loginResponse: response
720
+ });
721
+ }, createCoexistenceEmbeddedSignupConfig(input));
722
+ }),
723
+ teardown() {
724
+ input.target.removeEventListener("message", listener);
725
+ }
726
+ };
727
+ }
728
+ //#endregion
729
+ //#region src/coexistence/memory-store.ts
730
+ const IN_FLIGHT_SYNC_STATUSES = new Set(["requested", "processing"]);
731
+ function timeValue(value) {
732
+ if (!value) return;
733
+ return value instanceof Date ? value.getTime() : new Date(value).getTime();
734
+ }
735
+ function cloneRecord(record) {
736
+ return structuredClone(record);
737
+ }
738
+ function isInFlight(job, now) {
739
+ if (!IN_FLIGHT_SYNC_STATUSES.has(job.status)) return false;
740
+ const deadline = timeValue(job.deadlineAt);
741
+ if (deadline !== void 0 && deadline <= now.getTime()) return false;
742
+ return true;
743
+ }
744
+ var InMemoryCoexistenceStore = class {
745
+ connectedAccounts = /* @__PURE__ */ new Map();
746
+ onboardingSessions = /* @__PURE__ */ new Map();
747
+ syncJobs = /* @__PURE__ */ new Map();
748
+ contacts = /* @__PURE__ */ new Map();
749
+ lifecycleEvents = [];
750
+ rawEventStatuses = /* @__PURE__ */ new Map();
751
+ preflightStates = /* @__PURE__ */ new Map();
752
+ async upsertConnectedAccount(account) {
753
+ const record = cloneRecord(account);
754
+ this.connectedAccounts.set(account.wabaId, record);
755
+ this.connectedAccounts.set(account.phoneNumberId, record);
756
+ if (account.preflight) await this.upsertPreflightState(account.preflight);
757
+ }
758
+ async getConnectedAccountByWabaId(wabaId) {
759
+ return cloneRecord(this.connectedAccounts.get(wabaId) ?? null);
760
+ }
761
+ async getConnectedAccountByPhoneNumberId(phoneNumberId) {
762
+ return cloneRecord(this.connectedAccounts.get(phoneNumberId) ?? null);
763
+ }
764
+ async recordOnboardingSession(session) {
765
+ this.onboardingSessions.set(session.id, cloneRecord(session));
766
+ if (session.preflight) await this.upsertPreflightState(session.preflight);
767
+ }
768
+ async upsertPreflightState(state) {
769
+ const record = cloneRecord(state);
770
+ if (state.phoneNumberId) this.preflightStates.set(state.phoneNumberId, record);
771
+ if (state.wabaId) this.preflightStates.set(state.wabaId, record);
772
+ }
773
+ async getPreflightStateByPhoneNumberId(phoneNumberId) {
774
+ return cloneRecord(this.preflightStates.get(phoneNumberId) ?? null);
775
+ }
776
+ async createSyncJob(job) {
777
+ if (await this.getInFlightSyncJob({
778
+ phoneNumberId: job.phoneNumberId,
779
+ syncType: job.syncType,
780
+ now: job.requestedAt ?? job.createdAt
781
+ })) throw new Error(`Coexistence sync already in flight for ${job.phoneNumberId}:${job.syncType}`);
782
+ this.syncJobs.set(job.requestId, cloneRecord(job));
783
+ }
784
+ async getInFlightSyncJob(input) {
785
+ const now = input.now instanceof Date ? input.now : new Date(input.now ?? Date.now());
786
+ for (const job of this.syncJobs.values()) {
787
+ if (job.phoneNumberId !== input.phoneNumberId || job.syncType !== input.syncType) continue;
788
+ if (isInFlight(job, now)) return cloneRecord(job);
789
+ if (IN_FLIGHT_SYNC_STATUSES.has(job.status) && job.deadlineAt) await this.updateSyncJobByRequestId(job.requestId, {
790
+ status: "deadline_exceeded",
791
+ failedAt: now,
792
+ failureReason: "sync_deadline_exceeded"
793
+ });
794
+ }
795
+ return null;
796
+ }
797
+ async updateSyncJobByRequestId(requestId, patch) {
798
+ const current = this.syncJobs.get(requestId);
799
+ if (!current) return;
800
+ this.syncJobs.set(requestId, cloneRecord({
801
+ ...current,
802
+ ...patch
803
+ }));
804
+ }
805
+ async upsertContact(contact) {
806
+ const key = `${contact.phoneNumberId ?? ""}:${contact.waId}`;
807
+ this.contacts.set(key, cloneRecord(contact));
808
+ }
809
+ async removeContact(input) {
810
+ this.contacts.delete(`${input.phoneNumberId ?? ""}:${input.waId}`);
811
+ }
812
+ async recordLifecycleEvent(event) {
813
+ this.lifecycleEvents.push(cloneRecord(event));
814
+ }
815
+ async updateRawEventStatus(status) {
816
+ this.rawEventStatuses.set(status.id, cloneRecord(status));
817
+ }
818
+ };
819
+ //#endregion
460
820
  //#region src/services/message-logger.service.ts
461
821
  const WHATSAPP_MESSAGE_TYPES = [
462
822
  "queue_position",
@@ -511,7 +871,7 @@ var MessageLoggerService = class {
511
871
  * Log outgoing message for LGPD compliance
512
872
  */
513
873
  async logOutgoing(params) {
514
- const inserted = await this.store.createWhatsAppLog({
874
+ const { record: inserted } = await this.store.createWhatsAppLog({
515
875
  phone: params.phone,
516
876
  userId: params.userId,
517
877
  direction: "outgoing",
@@ -567,7 +927,7 @@ var MessageLoggerService = class {
567
927
  * Log incoming message (for audit trail)
568
928
  */
569
929
  async logIncoming(params) {
570
- const inserted = await this.store.createWhatsAppLog({
930
+ const { record: inserted, created } = await this.store.createWhatsAppLog({
571
931
  phone: params.phone,
572
932
  contactName: params.senderName,
573
933
  waMessageId: params.waMessageId,
@@ -578,12 +938,40 @@ var MessageLoggerService = class {
578
938
  metadata: params.metadata,
579
939
  sentAt: params.sentAt
580
940
  });
941
+ if (!created) return false;
942
+ const conversation = await this.getConversationById(inserted.conversationId);
943
+ if (conversation) await this.notify({
944
+ type: "NEW_MESSAGE",
945
+ message: inserted,
946
+ conversation
947
+ });
948
+ return true;
949
+ }
950
+ /**
951
+ * Log an imported WhatsApp message with an explicit direction and timestamp.
952
+ * Used by coexistence history imports and app echo webhooks where the message
953
+ * did not originate from the local send API call.
954
+ */
955
+ async logImportedMessage(params) {
956
+ const { record: inserted, created } = await this.store.createWhatsAppLog({
957
+ phone: params.phone,
958
+ contactName: params.senderName,
959
+ waMessageId: params.waMessageId,
960
+ direction: params.direction,
961
+ messageType: params.messageType ?? (params.direction === "incoming" ? "incoming" : "bot_reply"),
962
+ content: params.content,
963
+ status: params.direction === "incoming" ? "delivered" : "sent",
964
+ metadata: params.metadata,
965
+ sentAt: params.sentAt
966
+ });
967
+ if (!created) return false;
581
968
  const conversation = await this.getConversationById(inserted.conversationId);
582
969
  if (conversation) await this.notify({
583
970
  type: "NEW_MESSAGE",
584
971
  message: inserted,
585
972
  conversation
586
973
  });
974
+ return true;
587
975
  }
588
976
  };
589
977
  //#endregion
@@ -669,11 +1057,14 @@ function serializeTemplateParameter(parameter, value) {
669
1057
  }
670
1058
  //#endregion
671
1059
  exports.BetterZapClientError = require_client.BetterZapClientError;
1060
+ exports.CoexistenceService = CoexistenceService;
672
1061
  exports.EMPTY_TEMPLATE_REGISTRY = EMPTY_TEMPLATE_REGISTRY;
673
1062
  exports.FREEFORM_MESSAGE_WINDOW_MS = FREEFORM_MESSAGE_WINDOW_MS;
1063
+ exports.InMemoryCoexistenceStore = InMemoryCoexistenceStore;
674
1064
  exports.MessageLoggerService = MessageLoggerService;
675
1065
  exports.WHATSAPP_MESSAGE_TYPES = WHATSAPP_MESSAGE_TYPES;
676
1066
  exports.WhatsAppService = WhatsAppService;
1067
+ exports.createCoexistenceEmbeddedSignupConfig = createCoexistenceEmbeddedSignupConfig;
677
1068
  exports.createFreeformMessageWindow = createFreeformMessageWindow;
678
1069
  exports.createLogger = createLogger;
679
1070
  exports.createZapClient = require_client.createZapClient;
@@ -683,9 +1074,13 @@ exports.formatPhone = formatPhone;
683
1074
  exports.getLatestIncomingMessageAt = getLatestIncomingMessageAt;
684
1075
  exports.getTemplateNames = getTemplateNames;
685
1076
  exports.hasConfiguredTemplates = hasConfiguredTemplates;
1077
+ exports.launchCoexistenceEmbeddedSignup = launchCoexistenceEmbeddedSignup;
686
1078
  exports.noopLogger = noopLogger;
1079
+ exports.normalizeCoexistenceSessionEvent = normalizeCoexistenceSessionEvent;
1080
+ exports.normalizeCoexistenceSessionPayload = normalizeCoexistenceSessionPayload;
687
1081
  exports.normalizeConversationRecord = normalizeConversationRecord;
688
1082
  exports.normalizeConversationRecords = normalizeConversationRecords;
689
1083
  exports.resolveConversationFreeformMessageWindow = resolveConversationFreeformMessageWindow;
690
1084
  exports.serializeError = serializeError;
691
1085
  exports.serializeTemplateFromRegistry = serializeTemplateFromRegistry;
1086
+ exports.toLegacyCoexistenceSessionEvent = toLegacyCoexistenceSessionEvent;