@whanext/core 0.19.18 → 0.19.20

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.js CHANGED
@@ -2346,7 +2346,23 @@ var WhaNextApp = class {
2346
2346
  const messaging = provider?.messaging ?? {
2347
2347
  sent: 0,
2348
2348
  received: 0,
2349
- failed: 0
2349
+ failed: 0,
2350
+ decryptedPayloads: 0,
2351
+ unavailable: 0,
2352
+ resendRequested: 0,
2353
+ recovered: 0,
2354
+ recoveryFailed: 0,
2355
+ unavailableUnrecoverable: 0,
2356
+ decodeFailures: 0,
2357
+ unhandledStanzas: 0,
2358
+ ignoredOffline: 0,
2359
+ duplicates: 0,
2360
+ normalizationFailures: 0,
2361
+ transportFramesIn: 0,
2362
+ transportNodesIn: 0,
2363
+ transportDecodeErrors: 0,
2364
+ messageStanzasIn: 0,
2365
+ ingressStalls: 0
2350
2366
  };
2351
2367
  const crypto = provider?.crypto ?? {
2352
2368
  backend: "unknown",
@@ -2467,6 +2483,20 @@ var WhaNextApp = class {
2467
2483
  await this.#events.emit("groupMetadataRecovered", event.payload);
2468
2484
  } else if (event.type === "cryptoDegraded") {
2469
2485
  await this.#events.emit("cryptoDegraded", event.payload);
2486
+ } else if (event.type === "messageUnavailable") {
2487
+ await this.#events.emit("messageUnavailable", event.payload);
2488
+ } else if (event.type === "messageRecovered") {
2489
+ await this.#events.emit("messageRecovered", event.payload);
2490
+ } else if (event.type === "messageRecoveryFailed") {
2491
+ await this.#events.emit("messageRecoveryFailed", event.payload);
2492
+ } else if (event.type === "messageDecodeFailure") {
2493
+ await this.#events.emit("messageDecodeFailure", event.payload);
2494
+ } else if (event.type === "messageDiscarded") {
2495
+ await this.#events.emit("messageDiscarded", event.payload);
2496
+ } else if (event.type === "messageIngressStalled") {
2497
+ await this.#events.emit("messageIngressStalled", event.payload);
2498
+ } else if (event.type === "transportDecodeFailure") {
2499
+ await this.#events.emit("transportDecodeFailure", event.payload);
2470
2500
  }
2471
2501
  await this.#refreshHealthState();
2472
2502
  });
@@ -3136,11 +3166,14 @@ var sharedMediaProcessor = createMediaProcessor();
3136
3166
  var REMOTE_MEDIA_TIMEOUT_MS = 12e4;
3137
3167
  var DEFAULT_CONNECT_TIMEOUT_MS = 15e3;
3138
3168
  var DEFAULT_NODE_QUERY_TIMEOUT_MS = 3e4;
3169
+ var DEFAULT_INGRESS_STALL_TIMEOUT_MS = 1e4;
3139
3170
  var PROVIDER_DEGRADED_WINDOW_MS = 12e4;
3140
3171
  var GROUP_METADATA_CACHE_TTL_MS = 18e4;
3141
3172
  var DEVICE_LIST_CACHE_TTL_MS = 18e4;
3142
3173
  var GROUP_METADATA_RECOVERY_COOLDOWN_MS = 6e4;
3143
3174
  var GROUP_METADATA_MISMATCH_WARNING = "group message publish acknowledged with mismatch metadata";
3175
+ var MESSAGE_RECOVERY_TIMEOUT_MS = 3e4;
3176
+ var DECRYPT_CORRELATION_TTL_MS = 15e3;
3144
3177
  var messageSnapshotRetentionSeconds = 7 * 24 * 60 * 60;
3145
3178
  var messageSnapshotMaxPerSession = 2e4;
3146
3179
  var messageSnapshotPruneInterval = 256;
@@ -3151,6 +3184,9 @@ var ZapoProvider = class {
3151
3184
  #messageStore = /* @__PURE__ */ new Map();
3152
3185
  #messageKeyStore = /* @__PURE__ */ new WeakMap();
3153
3186
  #deliveredMessageStore = /* @__PURE__ */ new Set();
3187
+ #pendingUnavailableRecoveries = /* @__PURE__ */ new Map();
3188
+ #recentDecryptedPayloads = /* @__PURE__ */ new Map();
3189
+ #pendingIngressMessages = /* @__PURE__ */ new Map();
3154
3190
  #handledProtocolStore = /* @__PURE__ */ new Set();
3155
3191
  #callCreatorStore = /* @__PURE__ */ new Map();
3156
3192
  #groupMetadataRecoveryAt = /* @__PURE__ */ new Map();
@@ -3159,6 +3195,8 @@ var ZapoProvider = class {
3159
3195
  #cryptoBackend;
3160
3196
  #connectTimeoutMs;
3161
3197
  #nodeQueryTimeoutMs;
3198
+ #ingressDiagnostics;
3199
+ #ingressStallTimeoutMs;
3162
3200
  #protocolMutationQueue = Promise.resolve();
3163
3201
  #state = "idle";
3164
3202
  #client;
@@ -3181,6 +3219,22 @@ var ZapoProvider = class {
3181
3219
  #sentMessages = 0;
3182
3220
  #receivedMessages = 0;
3183
3221
  #failedMessages = 0;
3222
+ #decryptedPayloads = 0;
3223
+ #unavailableMessages = 0;
3224
+ #resendRequestedMessages = 0;
3225
+ #recoveredMessages = 0;
3226
+ #recoveryFailedMessages = 0;
3227
+ #unavailableUnrecoverableMessages = 0;
3228
+ #decodeFailures = 0;
3229
+ #unhandledStanzas = 0;
3230
+ #ignoredOfflineMessages = 0;
3231
+ #duplicateMessages = 0;
3232
+ #normalizationFailures = 0;
3233
+ #transportFramesIn = 0;
3234
+ #transportNodesIn = 0;
3235
+ #transportDecodeErrors = 0;
3236
+ #messageStanzasIn = 0;
3237
+ #ingressStalls = 0;
3184
3238
  #lastIncomingAt;
3185
3239
  #lastOutgoingAt;
3186
3240
  #decryptFailures = 0;
@@ -3198,6 +3252,8 @@ var ZapoProvider = class {
3198
3252
  this.#messageCacheSize = Math.max(1, options.messageCacheSize ?? 1e3);
3199
3253
  this.#connectTimeoutMs = normalizeProviderTimeout(options.connectTimeoutMs, DEFAULT_CONNECT_TIMEOUT_MS);
3200
3254
  this.#nodeQueryTimeoutMs = normalizeProviderTimeout(options.nodeQueryTimeoutMs, DEFAULT_NODE_QUERY_TIMEOUT_MS);
3255
+ this.#ingressDiagnostics = options.ingressDiagnostics === true;
3256
+ this.#ingressStallTimeoutMs = normalizeProviderTimeout(options.ingressStallTimeoutMs, DEFAULT_INGRESS_STALL_TIMEOUT_MS);
3201
3257
  this.#cryptoBackend = detectCryptoBackend();
3202
3258
  }
3203
3259
  on(event, listener) {
@@ -3221,6 +3277,22 @@ var ZapoProvider = class {
3221
3277
  sent: this.#sentMessages,
3222
3278
  received: this.#receivedMessages,
3223
3279
  failed: this.#failedMessages,
3280
+ decryptedPayloads: this.#decryptedPayloads,
3281
+ unavailable: this.#unavailableMessages,
3282
+ resendRequested: this.#resendRequestedMessages,
3283
+ recovered: this.#recoveredMessages,
3284
+ recoveryFailed: this.#recoveryFailedMessages,
3285
+ unavailableUnrecoverable: this.#unavailableUnrecoverableMessages,
3286
+ decodeFailures: this.#decodeFailures,
3287
+ unhandledStanzas: this.#unhandledStanzas,
3288
+ ignoredOffline: this.#ignoredOfflineMessages,
3289
+ duplicates: this.#duplicateMessages,
3290
+ normalizationFailures: this.#normalizationFailures,
3291
+ transportFramesIn: this.#transportFramesIn,
3292
+ transportNodesIn: this.#transportNodesIn,
3293
+ transportDecodeErrors: this.#transportDecodeErrors,
3294
+ messageStanzasIn: this.#messageStanzasIn,
3295
+ ingressStalls: this.#ingressStalls,
3224
3296
  ...this.#lastIncomingAt ? { lastIncomingAt: new Date(this.#lastIncomingAt) } : {},
3225
3297
  ...this.#lastOutgoingAt ? { lastOutgoingAt: new Date(this.#lastOutgoingAt) } : {}
3226
3298
  },
@@ -3267,6 +3339,15 @@ var ZapoProvider = class {
3267
3339
  clearTimeout(this.#healthRefreshTimer);
3268
3340
  this.#healthRefreshTimer = void 0;
3269
3341
  }
3342
+ for (const pending of this.#pendingUnavailableRecoveries.values()) {
3343
+ clearTimeout(pending.timer);
3344
+ }
3345
+ this.#pendingUnavailableRecoveries.clear();
3346
+ for (const pending of this.#pendingIngressMessages.values()) {
3347
+ clearTimeout(pending.timer);
3348
+ }
3349
+ this.#pendingIngressMessages.clear();
3350
+ this.#recentDecryptedPayloads.clear();
3270
3351
  const client = this.#client;
3271
3352
  this.#connectPromise = void 0;
3272
3353
  try {
@@ -3605,7 +3686,22 @@ var ZapoProvider = class {
3605
3686
  );
3606
3687
  void this.#stopTerminalConnection(client, error);
3607
3688
  });
3689
+ client.on("debug_transport_frame_in", ({ frame }) => {
3690
+ this.#transportFramesIn += 1;
3691
+ if (this.#ingressDiagnostics) {
3692
+ this.#logger.debug("Inbound WhatsApp transport frame received.", {
3693
+ frameBytes: frame.byteLength
3694
+ });
3695
+ }
3696
+ });
3697
+ client.on("debug_transport_node_in", ({ node }) => {
3698
+ this.#handleTransportNodeIn(node);
3699
+ });
3700
+ client.on("debug_transport_decode_error", ({ error, frame }) => {
3701
+ this.#handleTransportDecodeError(error, frame.byteLength);
3702
+ });
3608
3703
  client.on("message", (event) => {
3704
+ this.#resolveIngressMessage(event.key, "message_event");
3609
3705
  const protocol = this.#protocolMessage(event);
3610
3706
  if (protocol && this.#isMessageMutationProtocol(protocol.type)) {
3611
3707
  this.#enqueueProtocolEvent({
@@ -3628,6 +3724,16 @@ var ZapoProvider = class {
3628
3724
  }
3629
3725
  this.#handleMessage(event);
3630
3726
  });
3727
+ client.on("message_unavailable", (event) => {
3728
+ this.#resolveIngressMessage(event.key, "unavailable");
3729
+ this.#handleUnavailableMessage(event);
3730
+ });
3731
+ client.on("debug_decrypted_payload", (event) => {
3732
+ this.#handleDecryptedPayload(event);
3733
+ });
3734
+ client.on("debug_unhandled_stanza", (event) => {
3735
+ this.#handleUnhandledStanza(event);
3736
+ });
3631
3737
  client.on("message_send", (event) => {
3632
3738
  if (!event.id || !event.message) return;
3633
3739
  this.#sentMessages += 1;
@@ -3659,6 +3765,139 @@ var ZapoProvider = class {
3659
3765
  void this.#handleConnectionEvent(client, event);
3660
3766
  });
3661
3767
  }
3768
+ #handleTransportNodeIn(node) {
3769
+ this.#transportNodesIn += 1;
3770
+ if (node.tag !== "message") return;
3771
+ this.#messageStanzasIn += 1;
3772
+ const stanzaId = stringValue(node.attrs.id);
3773
+ const chatId = normalizeIngressJid(stringValue(node.attrs.from));
3774
+ const participantId = normalizeIngressJid(
3775
+ stringValue(node.attrs.participant) ?? stringValue(node.attrs.participant_pn) ?? stringValue(node.attrs.participant_lid) ?? stringValue(node.attrs.sender_pn) ?? stringValue(node.attrs.sender_lid)
3776
+ );
3777
+ const stanzaType = stringValue(node.attrs.type);
3778
+ const addressingMode = stringValue(node.attrs.addressing_mode);
3779
+ const children = Array.isArray(node.content) ? node.content : [];
3780
+ const childTags = [...new Set(children.map((child) => child.tag))];
3781
+ const encTypes = [...new Set(children.filter((child) => child.tag === "enc").map((child) => stringValue(child.attrs.type)).filter((value) => value !== void 0))];
3782
+ if (this.#ingressDiagnostics) {
3783
+ this.#logger.info("Inbound WhatsApp message stanza observed.", {
3784
+ ...stanzaId ? { stanzaId } : {},
3785
+ ...chatId ? { chatId } : {},
3786
+ ...participantId ? { participantId } : {},
3787
+ ...stanzaType ? { stanzaType } : {},
3788
+ ...addressingMode ? { addressingMode } : {},
3789
+ ...childTags.length > 0 ? { childTags } : {},
3790
+ ...encTypes.length > 0 ? { encTypes } : {}
3791
+ });
3792
+ }
3793
+ const correlationKey = this.#stanzaCorrelationKey(chatId, stanzaId);
3794
+ if (!correlationKey || !stanzaId) return;
3795
+ const previous = this.#pendingIngressMessages.get(correlationKey);
3796
+ if (previous) clearTimeout(previous.timer);
3797
+ const observedAt = Date.now();
3798
+ const timer = setTimeout(() => {
3799
+ const pending = this.#pendingIngressMessages.get(correlationKey);
3800
+ if (!pending || pending.observedAt !== observedAt) return;
3801
+ this.#pendingIngressMessages.delete(correlationKey);
3802
+ this.#ingressStalls += 1;
3803
+ this.#markDegraded();
3804
+ const occurredAt = /* @__PURE__ */ new Date();
3805
+ this.#emitStability({
3806
+ type: "messageIngressStalled",
3807
+ payload: {
3808
+ occurredAt,
3809
+ waitedMs: occurredAt.getTime() - pending.observedAt,
3810
+ lastStage: pending.lastStage,
3811
+ stanzaId: pending.stanzaId,
3812
+ ...pending.chatId ? { chatId: pending.chatId } : {},
3813
+ ...pending.participantId ? { participantId: pending.participantId } : {},
3814
+ ...pending.stanzaType ? { stanzaType: pending.stanzaType } : {},
3815
+ ...pending.addressingMode ? { addressingMode: pending.addressingMode } : {}
3816
+ }
3817
+ });
3818
+ this.#logger.warn("Inbound WhatsApp message stanza did not reach a terminal provider event.", {
3819
+ stanzaId: pending.stanzaId,
3820
+ ...pending.chatId ? { chatId: pending.chatId } : {},
3821
+ ...pending.participantId ? { participantId: pending.participantId } : {},
3822
+ lastStage: pending.lastStage,
3823
+ waitedMs: occurredAt.getTime() - pending.observedAt
3824
+ });
3825
+ }, this.#ingressStallTimeoutMs);
3826
+ this.#pendingIngressMessages.set(correlationKey, {
3827
+ observedAt,
3828
+ lastStage: "stanza_received",
3829
+ timer,
3830
+ stanzaId,
3831
+ ...chatId ? { chatId } : {},
3832
+ ...participantId ? { participantId } : {},
3833
+ ...stanzaType ? { stanzaType } : {},
3834
+ ...addressingMode ? { addressingMode } : {}
3835
+ });
3836
+ }
3837
+ #handleTransportDecodeError(error, frameBytes) {
3838
+ this.#transportDecodeErrors += 1;
3839
+ this.#markDegraded();
3840
+ const occurredAt = /* @__PURE__ */ new Date();
3841
+ this.#emitStability({
3842
+ type: "transportDecodeFailure",
3843
+ payload: {
3844
+ occurredAt,
3845
+ frameBytes,
3846
+ errorName: error.name || "Error",
3847
+ errorMessage: error.message || String(error)
3848
+ }
3849
+ });
3850
+ this.#logger.warn("Inbound WhatsApp transport frame could not be decoded.", {
3851
+ frameBytes,
3852
+ errorName: error.name || "Error",
3853
+ errorMessage: error.message || String(error)
3854
+ });
3855
+ }
3856
+ #markIngressDecrypted(chatId, stanzaId) {
3857
+ const correlationKey = this.#stanzaCorrelationKey(normalizeIngressJid(chatId), stanzaId);
3858
+ if (!correlationKey) return;
3859
+ const pending = this.#pendingIngressMessages.get(correlationKey);
3860
+ if (!pending) return;
3861
+ pending.lastStage = "decrypted";
3862
+ if (this.#ingressDiagnostics) {
3863
+ this.#logger.info("Inbound WhatsApp message payload decrypted.", {
3864
+ stanzaId: pending.stanzaId,
3865
+ ...pending.chatId ? { chatId: pending.chatId } : {},
3866
+ ...pending.participantId ? { participantId: pending.participantId } : {}
3867
+ });
3868
+ }
3869
+ }
3870
+ #resolveIngressMessage(key, outcome) {
3871
+ const chatId = normalizeIngressJid(key.remoteJid ?? void 0);
3872
+ const correlationKey = this.#stanzaCorrelationKey(chatId, key.id ?? void 0);
3873
+ if (!correlationKey) return;
3874
+ const pending = this.#pendingIngressMessages.get(correlationKey);
3875
+ if (!pending) return;
3876
+ clearTimeout(pending.timer);
3877
+ this.#pendingIngressMessages.delete(correlationKey);
3878
+ if (this.#ingressDiagnostics) {
3879
+ this.#logger.info("Inbound WhatsApp message stanza reached provider terminal stage.", {
3880
+ stanzaId: pending.stanzaId,
3881
+ ...pending.chatId ? { chatId: pending.chatId } : {},
3882
+ ...pending.participantId ? { participantId: pending.participantId } : {},
3883
+ lastStage: pending.lastStage,
3884
+ outcome,
3885
+ durationMs: Date.now() - pending.observedAt
3886
+ });
3887
+ }
3888
+ }
3889
+ #resolveIngressByStanza(chatId, stanzaId, outcome) {
3890
+ const normalizedChatId = normalizeIngressJid(chatId);
3891
+ const correlationKey = this.#stanzaCorrelationKey(normalizedChatId, stanzaId);
3892
+ if (!correlationKey) return;
3893
+ const pending = this.#pendingIngressMessages.get(correlationKey);
3894
+ if (!pending) return;
3895
+ const remoteJid = pending.chatId ?? normalizedChatId;
3896
+ this.#resolveIngressMessage({
3897
+ id: pending.stanzaId,
3898
+ ...remoteJid ? { remoteJid } : {}
3899
+ }, outcome);
3900
+ }
3662
3901
  #handleMessage(event) {
3663
3902
  const stored = event;
3664
3903
  if (stored.key?.id && stored.message) this.#remember(stored);
@@ -3666,7 +3905,16 @@ var ZapoProvider = class {
3666
3905
  if (quoted?.key.id && quoted.message) {
3667
3906
  this.#remember(quoted);
3668
3907
  }
3908
+ const deliveryKey = this.#messageDeliveryKey(stored.key);
3909
+ this.#resolveUnavailableRecovery(deliveryKey, stored.key);
3910
+ const decryptedCorrelationKey = this.#stanzaCorrelationKey(
3911
+ normalizeIngressJid(stored.key.remoteJid ?? void 0),
3912
+ stored.key.id ?? void 0
3913
+ );
3914
+ if (decryptedCorrelationKey) this.#recentDecryptedPayloads.delete(decryptedCorrelationKey);
3669
3915
  if (this.#isOfflineMessage(stored)) {
3916
+ this.#ignoredOfflineMessages += 1;
3917
+ this.#emitMessageDiscarded("offline", stored.key);
3670
3918
  this.#logger.debug("Ignored message queued before the current live connection.", {
3671
3919
  messageId: stored.key.id ?? void 0,
3672
3920
  chatId: stored.key.remoteJid ?? void 0,
@@ -3674,8 +3922,9 @@ var ZapoProvider = class {
3674
3922
  });
3675
3923
  return;
3676
3924
  }
3677
- const deliveryKey = this.#messageDeliveryKey(stored.key);
3678
3925
  if (stored.key.id && this.#deliveredMessageStore.has(deliveryKey)) {
3926
+ this.#duplicateMessages += 1;
3927
+ this.#emitMessageDiscarded("duplicate", stored.key);
3679
3928
  this.#logger.debug("Ignored duplicate Zapo message event.", {
3680
3929
  messageId: stored.key.id,
3681
3930
  chatId: stored.key.remoteJid ?? void 0
@@ -3683,10 +3932,28 @@ var ZapoProvider = class {
3683
3932
  return;
3684
3933
  }
3685
3934
  const message = normalizeZapoMessage(event);
3686
- if (!message) return;
3935
+ if (!message) {
3936
+ this.#normalizationFailures += 1;
3937
+ this.#emitMessageDiscarded("normalization_failed", stored.key);
3938
+ this.#logger.warn("Could not normalize incoming Zapo message.", {
3939
+ messageId: stored.key.id ?? void 0,
3940
+ chatId: stored.key.remoteJid ?? void 0
3941
+ });
3942
+ return;
3943
+ }
3687
3944
  if (stored.key.id) this.#rememberDeliveredMessage(deliveryKey);
3688
3945
  this.#receivedMessages += 1;
3689
3946
  this.#lastIncomingAt = /* @__PURE__ */ new Date();
3947
+ if (this.#ingressDiagnostics) {
3948
+ this.#logger.info("Inbound WhatsApp message emitted to WhaNext.", {
3949
+ messageId: message.id,
3950
+ chatId: message.chatId,
3951
+ userId: message.sender.id,
3952
+ contentKind: message.contentKind,
3953
+ hasMedia: message.media !== void 0,
3954
+ hasQuoted: message.quoted !== void 0
3955
+ });
3956
+ }
3690
3957
  this.#messageKeyStore.set(message.keys, stored);
3691
3958
  if (message.quoted && quoted?.message) {
3692
3959
  this.#messageKeyStore.set(
@@ -3696,6 +3963,166 @@ var ZapoProvider = class {
3696
3963
  }
3697
3964
  void this.#events.emit("message", message);
3698
3965
  }
3966
+ #handleUnavailableMessage(event) {
3967
+ this.#unavailableMessages += 1;
3968
+ const occurredAt = /* @__PURE__ */ new Date();
3969
+ const messageId = event.key.id || void 0;
3970
+ const chatId = event.key.remoteJid || void 0;
3971
+ const participantId = event.key.participant ?? event.key.participantAlt ?? void 0;
3972
+ this.#emitStability({
3973
+ type: "messageUnavailable",
3974
+ payload: {
3975
+ kind: event.kind,
3976
+ resendRequested: event.resendRequested,
3977
+ occurredAt,
3978
+ ...messageId ? { messageId } : {},
3979
+ ...chatId ? { chatId } : {},
3980
+ ...participantId ? { participantId } : {}
3981
+ }
3982
+ });
3983
+ if (!event.resendRequested) {
3984
+ this.#unavailableUnrecoverableMessages += 1;
3985
+ const log = event.kind === "other" ? this.#logger.warn.bind(this.#logger) : this.#logger.debug.bind(this.#logger);
3986
+ log("Incoming message is unavailable and was not queued for recovery.", {
3987
+ kind: event.kind,
3988
+ ...messageId ? { messageId } : {},
3989
+ ...chatId ? { chatId } : {},
3990
+ ...participantId ? { participantId } : {}
3991
+ });
3992
+ return;
3993
+ }
3994
+ this.#resendRequestedMessages += 1;
3995
+ const deliveryKey = this.#messageDeliveryKey(event.key);
3996
+ const previous = this.#pendingUnavailableRecoveries.get(deliveryKey);
3997
+ if (previous) clearTimeout(previous.timer);
3998
+ const requestedAt = Date.now();
3999
+ const timer = setTimeout(() => {
4000
+ const pending = this.#pendingUnavailableRecoveries.get(deliveryKey);
4001
+ if (!pending || pending.requestedAt !== requestedAt) return;
4002
+ this.#pendingUnavailableRecoveries.delete(deliveryKey);
4003
+ this.#recoveryFailedMessages += 1;
4004
+ this.#markDegraded();
4005
+ const failedAt = /* @__PURE__ */ new Date();
4006
+ this.#emitStability({
4007
+ type: "messageRecoveryFailed",
4008
+ payload: {
4009
+ failedAt,
4010
+ waitedMs: failedAt.getTime() - pending.requestedAt,
4011
+ ...pending.messageId ? { messageId: pending.messageId } : {},
4012
+ ...pending.chatId ? { chatId: pending.chatId } : {},
4013
+ ...pending.participantId ? { participantId: pending.participantId } : {}
4014
+ }
4015
+ });
4016
+ this.#logger.warn("Unavailable message recovery did not arrive in time.", {
4017
+ ...pending.messageId ? { messageId: pending.messageId } : {},
4018
+ ...pending.chatId ? { chatId: pending.chatId } : {},
4019
+ waitedMs: failedAt.getTime() - pending.requestedAt
4020
+ });
4021
+ }, MESSAGE_RECOVERY_TIMEOUT_MS);
4022
+ this.#pendingUnavailableRecoveries.set(deliveryKey, {
4023
+ requestedAt,
4024
+ timer,
4025
+ ...messageId ? { messageId } : {},
4026
+ ...chatId ? { chatId } : {},
4027
+ ...participantId ? { participantId } : {}
4028
+ });
4029
+ this.#logger.info("Incoming message unavailable; primary-device resend requested.", {
4030
+ ...messageId ? { messageId } : {},
4031
+ ...chatId ? { chatId } : {},
4032
+ ...participantId ? { participantId } : {}
4033
+ });
4034
+ }
4035
+ #resolveUnavailableRecovery(deliveryKey, key) {
4036
+ const pending = this.#pendingUnavailableRecoveries.get(deliveryKey);
4037
+ if (!pending) return;
4038
+ clearTimeout(pending.timer);
4039
+ this.#pendingUnavailableRecoveries.delete(deliveryKey);
4040
+ this.#recoveredMessages += 1;
4041
+ const recoveredAt = /* @__PURE__ */ new Date();
4042
+ const participantId = key.participant ?? key.participantAlt ?? pending.participantId;
4043
+ this.#emitStability({
4044
+ type: "messageRecovered",
4045
+ payload: {
4046
+ recoveredAt,
4047
+ recoveryMs: recoveredAt.getTime() - pending.requestedAt,
4048
+ ...key.id ? { messageId: key.id } : pending.messageId ? { messageId: pending.messageId } : {},
4049
+ ...key.remoteJid ? { chatId: key.remoteJid } : pending.chatId ? { chatId: pending.chatId } : {},
4050
+ ...participantId ? { participantId } : {}
4051
+ }
4052
+ });
4053
+ this.#logger.info("Recovered unavailable message from the primary device.", {
4054
+ ...key.id ? { messageId: key.id } : {},
4055
+ ...key.remoteJid ? { chatId: key.remoteJid } : {},
4056
+ recoveryMs: recoveredAt.getTime() - pending.requestedAt
4057
+ });
4058
+ }
4059
+ #handleDecryptedPayload(event) {
4060
+ this.#decryptedPayloads += 1;
4061
+ this.#markIngressDecrypted(event.chatJid, event.stanzaId);
4062
+ const correlationKey = this.#stanzaCorrelationKey(normalizeIngressJid(event.chatJid), event.stanzaId);
4063
+ if (!correlationKey) return;
4064
+ const now = Date.now();
4065
+ this.#recentDecryptedPayloads.set(correlationKey, {
4066
+ observedAt: now,
4067
+ encType: event.encType
4068
+ });
4069
+ for (const [key, value] of this.#recentDecryptedPayloads) {
4070
+ if (now - value.observedAt > DECRYPT_CORRELATION_TTL_MS) {
4071
+ this.#recentDecryptedPayloads.delete(key);
4072
+ }
4073
+ }
4074
+ }
4075
+ #handleUnhandledStanza(event) {
4076
+ this.#unhandledStanzas += 1;
4077
+ const normalizedChatId = normalizeIngressJid(event.chatJid);
4078
+ const correlationKey = this.#stanzaCorrelationKey(normalizedChatId, event.stanzaId);
4079
+ const decrypted = correlationKey ? this.#recentDecryptedPayloads.get(correlationKey) : void 0;
4080
+ const now = Date.now();
4081
+ if (decrypted && now - decrypted.observedAt <= DECRYPT_CORRELATION_TTL_MS) {
4082
+ this.#decodeFailures += 1;
4083
+ this.#resolveIngressByStanza(normalizedChatId, event.stanzaId, "decode_failure");
4084
+ this.#markDegraded();
4085
+ this.#emitStability({
4086
+ type: "messageDecodeFailure",
4087
+ payload: {
4088
+ occurredAt: new Date(now),
4089
+ reason: event.reason,
4090
+ encType: decrypted.encType,
4091
+ ...event.stanzaId ? { stanzaId: event.stanzaId } : {},
4092
+ ...event.chatJid ? { chatId: event.chatJid } : {}
4093
+ }
4094
+ });
4095
+ this.#logger.warn("Decrypted incoming payload could not be decoded into a supported stanza.", {
4096
+ reason: event.reason,
4097
+ encType: decrypted.encType,
4098
+ ...event.stanzaId ? { stanzaId: event.stanzaId } : {},
4099
+ ...event.chatJid ? { chatId: event.chatJid } : {}
4100
+ });
4101
+ if (correlationKey) this.#recentDecryptedPayloads.delete(correlationKey);
4102
+ return;
4103
+ }
4104
+ this.#resolveIngressByStanza(normalizedChatId, event.stanzaId, "decode_failure");
4105
+ this.#logger.debug("Incoming stanza was not handled by Zapo.", {
4106
+ reason: event.reason,
4107
+ ...event.stanzaId ? { stanzaId: event.stanzaId } : {},
4108
+ ...event.chatJid ? { chatId: event.chatJid } : {}
4109
+ });
4110
+ }
4111
+ #emitMessageDiscarded(reason, key) {
4112
+ this.#emitStability({
4113
+ type: "messageDiscarded",
4114
+ payload: {
4115
+ reason,
4116
+ occurredAt: /* @__PURE__ */ new Date(),
4117
+ ...key.id ? { messageId: key.id } : {},
4118
+ ...key.remoteJid ? { chatId: key.remoteJid } : {}
4119
+ }
4120
+ });
4121
+ }
4122
+ #stanzaCorrelationKey(chatJid, stanzaId) {
4123
+ if (!stanzaId) return void 0;
4124
+ return `${chatJid ?? ""}:${stanzaId}`;
4125
+ }
3699
4126
  #handleAddonEvent(event) {
3700
4127
  const decrypted = this.#addonRecord(event.decrypted);
3701
4128
  const protocol = this.#addonProtocolMessage(decrypted);
@@ -3953,6 +4380,11 @@ var ZapoProvider = class {
3953
4380
  }
3954
4381
  if (message === "failed to decrypt incoming message") {
3955
4382
  this.#decryptFailures += 1;
4383
+ this.#resolveIngressByStanza(
4384
+ stringValue(context.from) ?? stringValue(context.groupJid),
4385
+ stringValue(context.id),
4386
+ "decrypt_failure"
4387
+ );
3956
4388
  const detail = stringValue(context.message);
3957
4389
  const kind = detail === "sender key id mismatch" ? "sender_key_mismatch" : "decrypt_failure";
3958
4390
  if (kind === "sender_key_mismatch") this.#senderKeyMismatches += 1;
@@ -4433,11 +4865,16 @@ var ZapoProvider = class {
4433
4865
  return mentions.map((mention) => typeof mention === "string" ? mention : mention.mentionId);
4434
4866
  }
4435
4867
  #toZapoKey(key) {
4868
+ const original = this.#messageKeyStore.get(key)?.key;
4869
+ const participant = original?.participant ?? key.participantId;
4436
4870
  return {
4437
4871
  id: key.id,
4438
4872
  remoteJid: key.chatId,
4439
4873
  fromMe: key.fromMe,
4440
- ...key.participantId ? { participant: key.participantId } : {}
4874
+ ...original?.remoteJidAlt ? { remoteJidAlt: original.remoteJidAlt } : {},
4875
+ ...participant ? { participant } : {},
4876
+ ...original?.participantAlt ? { participantAlt: original.participantAlt } : {},
4877
+ ...original?.addressingMode ? { addressingMode: original.addressingMode } : {}
4441
4878
  };
4442
4879
  }
4443
4880
  #sent(result, chatId) {
@@ -5113,6 +5550,13 @@ function normalizeProviderTimeout(value, fallback) {
5113
5550
  if (value === void 0 || !Number.isFinite(value)) return fallback;
5114
5551
  return Math.max(1e3, Math.floor(value));
5115
5552
  }
5553
+ function normalizeIngressJid(value) {
5554
+ if (!value) return void 0;
5555
+ const at = value.indexOf("@");
5556
+ if (at <= 0) return value;
5557
+ const local = value.slice(0, at).replace(/:\d+$/, "");
5558
+ return `${local}${value.slice(at)}`;
5559
+ }
5116
5560
  function detectCryptoBackend() {
5117
5561
  const requested = process.env.ZAPO_NATIVE_BACKEND?.trim().toLowerCase() ?? "auto";
5118
5562
  if (requested === "js" || requested === "none") return "js";
@@ -5213,7 +5657,9 @@ async function create(options = {}) {
5213
5657
  ...options.processOfflineMessages !== void 0 ? { processOfflineMessages: options.processOfflineMessages } : {},
5214
5658
  ...options.reconnect ? { reconnect: options.reconnect } : {},
5215
5659
  ...options.providerTimeouts?.connectTimeoutMs !== void 0 ? { connectTimeoutMs: options.providerTimeouts.connectTimeoutMs } : {},
5216
- ...options.providerTimeouts?.nodeQueryTimeoutMs !== void 0 ? { nodeQueryTimeoutMs: options.providerTimeouts.nodeQueryTimeoutMs } : {}
5660
+ ...options.providerTimeouts?.nodeQueryTimeoutMs !== void 0 ? { nodeQueryTimeoutMs: options.providerTimeouts.nodeQueryTimeoutMs } : {},
5661
+ ...options.providerDiagnostics?.ingress !== void 0 ? { ingressDiagnostics: options.providerDiagnostics.ingress } : {},
5662
+ ...options.providerDiagnostics?.ingressStallTimeoutMs !== void 0 ? { ingressStallTimeoutMs: options.providerDiagnostics.ingressStallTimeoutMs } : {}
5217
5663
  });
5218
5664
  return new WhaNextApp(provider, {
5219
5665
  ...options.accountId ? { accountId: options.accountId } : {},
@@ -5451,6 +5897,12 @@ function mergeCreateOptions(shared, account) {
5451
5897
  ...account.providerTimeouts
5452
5898
  };
5453
5899
  }
5900
+ if (shared.providerDiagnostics || account.providerDiagnostics) {
5901
+ merged.providerDiagnostics = {
5902
+ ...shared.providerDiagnostics,
5903
+ ...account.providerDiagnostics
5904
+ };
5905
+ }
5454
5906
  if (shared.logger && account.logger && typeof shared.logger === "object" && typeof account.logger === "object") {
5455
5907
  merged.logger = {
5456
5908
  ...shared.logger,