@whanext/core 0.19.18 → 0.19.19

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/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.19.19
4
+
5
+ ### Provider
6
+ - Updated the official provider from `zapo-js@1.7.1` to `zapo-js@1.8.0`.
7
+ - Kept full received LID/PN addressing metadata when replying, reacting, editing, revoking, and pinning messages.
8
+ - Inherits Zapo 1.8.0 mailbox `fromMe` persistence fixes, group-status stanza attribute unwrapping, username support, expired-CDN media resend support, and mobile-primary WhatsApp Business support.
9
+
10
+ ### Incoming message observability
11
+ - Added `message_unavailable` tracking with primary-device resend correlation and a bounded 30-second recovery window.
12
+ - Added `debug_decrypted_payload` correlation with `debug_unhandled_stanza` so decrypted-but-undecodable payloads are visible without logging plaintext bytes.
13
+ - Expanded `health().messaging` with `decryptedPayloads`, `unavailable`, `resendRequested`, `recovered`, `recoveryFailed`, `unavailableUnrecoverable`, `decodeFailures`, `unhandledStanzas`, `ignoredOffline`, `duplicates`, and `normalizationFailures`.
14
+ - Added typed `messageUnavailable`, `messageRecovered`, `messageRecoveryFailed`, `messageDecodeFailure`, and `messageDiscarded` application events.
15
+ - Recovery failures and decrypted-payload decode failures temporarily mark provider stability as `degraded`; expected unrecoverable placeholders such as consumed view-once messages are counted without automatically degrading the session.
16
+
17
+ ### Safety
18
+ - Raw decrypted payload bytes from Zapo are never emitted through the WhaNext public events or written to logs.
19
+ - Existing `received`, `sent`, and `failed` counters keep their previous semantics; the new counters explain why an inbound stanza may not have become a public WhaNext `Message`.
20
+
3
21
  ## 0.19.18
4
22
 
5
23
  ### Stability
package/README.md CHANGED
@@ -331,7 +331,21 @@ console.log(health.timeouts);
331
331
 
332
332
  `status` continua representando o ciclo da aplicação: `idle`, `starting`, `ready` ou `stopped`. `stability` representa a saúde operacional do provider: `healthy`, `degraded`, `reconnecting` ou `offline`.
333
333
 
334
- O provider Zapo contabiliza reconexões, mensagens enviadas/recebidas, falhas de envio, falhas de descriptografia, `sender key id mismatch`, falhas de addons, divergências de `phash` e recuperações de metadata. O router contabiliza execuções ativas, fila atual, expirações e rejeições por fila cheia.
334
+ O provider Zapo contabiliza reconexões, mensagens enviadas/recebidas, falhas de envio, falhas de descriptografia, `sender key id mismatch`, falhas de addons, divergências de `phash` e recuperações de metadata. Com Zapo 1.8.0, `health.messaging` também mostra mensagens indisponíveis, pedidos de resend, recuperações, payloads descriptografados, falhas de decode, stanzas não tratadas, duplicatas, mensagens offline ignoradas e falhas de normalização. O router contabiliza execuções ativas, fila atual, expirações e rejeições por fila cheia.
335
+
336
+ ```ts
337
+ const { messaging } = app.health();
338
+
339
+ console.log(messaging.unavailable);
340
+ console.log(messaging.resendRequested);
341
+ console.log(messaging.recovered);
342
+ console.log(messaging.recoveryFailed);
343
+ console.log(messaging.decodeFailures);
344
+ console.log(messaging.ignoredOffline);
345
+ console.log(messaging.duplicates);
346
+ ```
347
+
348
+ `decryptedPayloads` conta payloads `<enc>` individuais, portanto pode ser maior que `received` em fanout para vários devices. O WhaNext usa esse sinal apenas para correlação e nunca expõe os bytes descriptografados no health, eventos públicos ou logs.
335
349
 
336
350
  ```ts
337
351
  server.get('/health', async () => app.health());
@@ -390,6 +404,26 @@ app.on('cryptoDegraded', ({ kind, chatId }) => {
390
404
  console.log(kind, chatId);
391
405
  });
392
406
 
407
+ app.on('messageUnavailable', ({ messageId, resendRequested }) => {
408
+ console.log(messageId, resendRequested);
409
+ });
410
+
411
+ app.on('messageRecovered', ({ messageId, recoveryMs }) => {
412
+ console.log(messageId, recoveryMs);
413
+ });
414
+
415
+ app.on('messageRecoveryFailed', ({ messageId, waitedMs }) => {
416
+ console.log(messageId, waitedMs);
417
+ });
418
+
419
+ app.on('messageDecodeFailure', ({ stanzaId, reason }) => {
420
+ console.log(stanzaId, reason);
421
+ });
422
+
423
+ app.on('messageDiscarded', ({ messageId, reason }) => {
424
+ console.log(messageId, reason);
425
+ });
426
+
393
427
  app.on('commandQueueTimeout', ({ command, queuedForMs }) => {
394
428
  console.log(command, queuedForMs);
395
429
  });
package/dist/index.d.ts CHANGED
@@ -315,6 +315,17 @@ interface ProviderMessagingHealth {
315
315
  sent: number;
316
316
  received: number;
317
317
  failed: number;
318
+ decryptedPayloads: number;
319
+ unavailable: number;
320
+ resendRequested: number;
321
+ recovered: number;
322
+ recoveryFailed: number;
323
+ unavailableUnrecoverable: number;
324
+ decodeFailures: number;
325
+ unhandledStanzas: number;
326
+ ignoredOffline: number;
327
+ duplicates: number;
328
+ normalizationFailures: number;
318
329
  lastIncomingAt?: Date;
319
330
  lastOutgoingAt?: Date;
320
331
  }
@@ -354,12 +365,63 @@ interface CryptoDegradedEvent {
354
365
  chatId?: string;
355
366
  participantId?: string;
356
367
  }
368
+ interface MessageUnavailableEvent {
369
+ kind: 'view_once' | 'hosted' | 'bot' | 'other';
370
+ resendRequested: boolean;
371
+ occurredAt: Date;
372
+ messageId?: string;
373
+ chatId?: string;
374
+ participantId?: string;
375
+ }
376
+ interface MessageRecoveredEvent {
377
+ recoveredAt: Date;
378
+ recoveryMs: number;
379
+ messageId?: string;
380
+ chatId?: string;
381
+ participantId?: string;
382
+ }
383
+ interface MessageRecoveryFailedEvent {
384
+ failedAt: Date;
385
+ waitedMs: number;
386
+ messageId?: string;
387
+ chatId?: string;
388
+ participantId?: string;
389
+ }
390
+ interface MessageDecodeFailureEvent {
391
+ occurredAt: Date;
392
+ reason: string;
393
+ stanzaId?: string;
394
+ chatId?: string;
395
+ encType?: string;
396
+ }
397
+ type MessageDiscardReason = 'offline' | 'duplicate' | 'normalization_failed';
398
+ interface MessageDiscardedEvent {
399
+ reason: MessageDiscardReason;
400
+ occurredAt: Date;
401
+ messageId?: string;
402
+ chatId?: string;
403
+ }
357
404
  type ProviderStabilityEvent = {
358
405
  type: 'groupMetadataRecovered';
359
406
  payload: GroupMetadataRecoveredEvent;
360
407
  } | {
361
408
  type: 'cryptoDegraded';
362
409
  payload: CryptoDegradedEvent;
410
+ } | {
411
+ type: 'messageUnavailable';
412
+ payload: MessageUnavailableEvent;
413
+ } | {
414
+ type: 'messageRecovered';
415
+ payload: MessageRecoveredEvent;
416
+ } | {
417
+ type: 'messageRecoveryFailed';
418
+ payload: MessageRecoveryFailedEvent;
419
+ } | {
420
+ type: 'messageDecodeFailure';
421
+ payload: MessageDecodeFailureEvent;
422
+ } | {
423
+ type: 'messageDiscarded';
424
+ payload: MessageDiscardedEvent;
363
425
  } | {
364
426
  type: 'healthRefresh';
365
427
  payload: {
@@ -936,6 +998,11 @@ interface AppEvents {
936
998
  connectionRecovered: ConnectionRecoveredEvent;
937
999
  groupMetadataRecovered: GroupMetadataRecoveredEvent;
938
1000
  cryptoDegraded: CryptoDegradedEvent;
1001
+ messageUnavailable: MessageUnavailableEvent;
1002
+ messageRecovered: MessageRecoveredEvent;
1003
+ messageRecoveryFailed: MessageRecoveryFailedEvent;
1004
+ messageDecodeFailure: MessageDecodeFailureEvent;
1005
+ messageDiscarded: MessageDiscardedEvent;
939
1006
  commandQueueTimeout: CommandQueueTimeoutEvent;
940
1007
  commandQueueFull: CommandQueueFullEvent;
941
1008
  }
@@ -1105,4 +1172,4 @@ declare class SqliteMuteStore implements MuteStore {
1105
1172
  close(): void;
1106
1173
  }
1107
1174
 
1108
- export { AccountService, type AddMuteOptions, type AddMuteResult, type AppEvents, type AppHealth, type AppHealthStatus, ArgsParser, type AudioContent, type BooleanOption, Browser, type ButtonsContent, type CacheOptions, type CacheStore, type CallEvent, type CallStatus, type ChangeResult, type CommandAccountContext, type CommandCatalogOptions, type CommandCatalogView, type CommandChatContext, type CommandConcurrency, type CommandConcurrencyContext, type CommandConcurrencyEvent, type CommandConcurrencyHealth, type CommandContext, type CommandCooldown, type CommandDefinition, type CommandErrorHandler, type CommandGroupContext, type CommandGroupDefinition, type CommandGuard, type CommandHelpOptions, type CommandHooks, type CommandLocalization, type CommandMetadata, type CommandMiddleware, type CommandOptionDefinition, type CommandOptionSchema, type CommandOptionValue, type CommandOptionValues, type CommandQueueFullEvent, type CommandQueueTimeoutEvent, type CommandRegistrar, CommandRouter, type CommandRouterEvents, type CommandRuntimeServices, type CommandScope, type ConcurrencyStrategy, type ConnectionRecoveredEvent, type ConnectionState, type ConnectionUpdate, type CopyCodeButton, type CreateMultiOptions, type CreateOptions, type CryptoAccelerationBackend, type CryptoDegradationKind, type CryptoDegradedEvent, DeferredReply, type DownloadedMedia, type DurationOption, type EnumOption, type ExecutableCommandDefinition, type GroupAccess, type GroupAddressingMode, type GroupMetadataRecoveredEvent, type GroupParticipant, type GroupParticipantAction, type GroupParticipantsChanged, type GroupRole, type GroupSnapshot, type GuardResult, type HealthChangedEvent, type ImageContent, type InteractiveResponse, type InteractiveResponseKind, type InviteResult, type LinkButton, type ListContent, type ListRow, type ListSection, type LoadCommandsOptions, type LoadCommandsResult, type LoadedCommand, type LogContext, type LogEntry, type LogFormat, type LogLevel, type LogWriter, Logger, type LoggerConfig, type LoggerOptions, type LoginOptions, type MediaKind, type MediaSource, type MemberActionState, MemoryCache, type MemoryCacheStats, type MentionTarget, type Message, type MessageButton, type MessageContent, type MessageContentKind, type MessageDeleted, type MessageEdited, type MessageKey, type MessageMedia, type MessagePayloadKind, type MessageProtocolKind, type MultiAccountOptions, type MultiAppEvent, type MultiAppHealth, MultiCommandRouter, type MultiLoadCommandsResult, type MultiLoginOptions, type MuteChangeResult, type MuteEnforcement, type MuteOptions, type MuteRecord, MuteService, type MuteStore, type NumberOption, ParsedCommandOptions, type ParticipantUpdateResult, type PollContent, type PresenceState, type ProviderConnectionHealth, type ProviderCryptoHealth, type ProviderGroupHealth, type ProviderHealth, type ProviderMessagingHealth, type ProviderStabilityEvent, type ProviderTimeoutHealth, type ProviderTimeoutOptions, type QuickReplyButton, type QuotedMessage, type ReconnectOptions, type RegisteredCommand, type RemoveMuteResult, type ReplyOptions, type RepostMessageOptions, type RouterOptions, type SentMessage, SqliteMuteStore, type StabilityHealthStatus, type StickerContent, type StoredMute, type StringOption, type TextContent, User, type UserData, type UserOption, type VideoContent, WhaNextApp, WhaNextError, type WhaNextErrorCode, WhaNextMultiApp, type WhatsAppProvider, create, createMulti, defineCommand, defineCommandGroup, defineCommands, defineSubcommand, guards, isCommandGroup, loadCommands, option, toWhaNextError };
1175
+ export { AccountService, type AddMuteOptions, type AddMuteResult, type AppEvents, type AppHealth, type AppHealthStatus, ArgsParser, type AudioContent, type BooleanOption, Browser, type ButtonsContent, type CacheOptions, type CacheStore, type CallEvent, type CallStatus, type ChangeResult, type CommandAccountContext, type CommandCatalogOptions, type CommandCatalogView, type CommandChatContext, type CommandConcurrency, type CommandConcurrencyContext, type CommandConcurrencyEvent, type CommandConcurrencyHealth, type CommandContext, type CommandCooldown, type CommandDefinition, type CommandErrorHandler, type CommandGroupContext, type CommandGroupDefinition, type CommandGuard, type CommandHelpOptions, type CommandHooks, type CommandLocalization, type CommandMetadata, type CommandMiddleware, type CommandOptionDefinition, type CommandOptionSchema, type CommandOptionValue, type CommandOptionValues, type CommandQueueFullEvent, type CommandQueueTimeoutEvent, type CommandRegistrar, CommandRouter, type CommandRouterEvents, type CommandRuntimeServices, type CommandScope, type ConcurrencyStrategy, type ConnectionRecoveredEvent, type ConnectionState, type ConnectionUpdate, type CopyCodeButton, type CreateMultiOptions, type CreateOptions, type CryptoAccelerationBackend, type CryptoDegradationKind, type CryptoDegradedEvent, DeferredReply, type DownloadedMedia, type DurationOption, type EnumOption, type ExecutableCommandDefinition, type GroupAccess, type GroupAddressingMode, type GroupMetadataRecoveredEvent, type GroupParticipant, type GroupParticipantAction, type GroupParticipantsChanged, type GroupRole, type GroupSnapshot, type GuardResult, type HealthChangedEvent, type ImageContent, type InteractiveResponse, type InteractiveResponseKind, type InviteResult, type LinkButton, type ListContent, type ListRow, type ListSection, type LoadCommandsOptions, type LoadCommandsResult, type LoadedCommand, type LogContext, type LogEntry, type LogFormat, type LogLevel, type LogWriter, Logger, type LoggerConfig, type LoggerOptions, type LoginOptions, type MediaKind, type MediaSource, type MemberActionState, MemoryCache, type MemoryCacheStats, type MentionTarget, type Message, type MessageButton, type MessageContent, type MessageContentKind, type MessageDecodeFailureEvent, type MessageDeleted, type MessageDiscardReason, type MessageDiscardedEvent, type MessageEdited, type MessageKey, type MessageMedia, type MessagePayloadKind, type MessageProtocolKind, type MessageRecoveredEvent, type MessageRecoveryFailedEvent, type MessageUnavailableEvent, type MultiAccountOptions, type MultiAppEvent, type MultiAppHealth, MultiCommandRouter, type MultiLoadCommandsResult, type MultiLoginOptions, type MuteChangeResult, type MuteEnforcement, type MuteOptions, type MuteRecord, MuteService, type MuteStore, type NumberOption, ParsedCommandOptions, type ParticipantUpdateResult, type PollContent, type PresenceState, type ProviderConnectionHealth, type ProviderCryptoHealth, type ProviderGroupHealth, type ProviderHealth, type ProviderMessagingHealth, type ProviderStabilityEvent, type ProviderTimeoutHealth, type ProviderTimeoutOptions, type QuickReplyButton, type QuotedMessage, type ReconnectOptions, type RegisteredCommand, type RemoveMuteResult, type ReplyOptions, type RepostMessageOptions, type RouterOptions, type SentMessage, SqliteMuteStore, type StabilityHealthStatus, type StickerContent, type StoredMute, type StringOption, type TextContent, User, type UserData, type UserOption, type VideoContent, WhaNextApp, WhaNextError, type WhaNextErrorCode, WhaNextMultiApp, type WhatsAppProvider, create, createMulti, defineCommand, defineCommandGroup, defineCommands, defineSubcommand, guards, isCommandGroup, loadCommands, option, toWhaNextError };
package/dist/index.js CHANGED
@@ -2346,7 +2346,18 @@ 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
2350
2361
  };
2351
2362
  const crypto = provider?.crypto ?? {
2352
2363
  backend: "unknown",
@@ -2467,6 +2478,16 @@ var WhaNextApp = class {
2467
2478
  await this.#events.emit("groupMetadataRecovered", event.payload);
2468
2479
  } else if (event.type === "cryptoDegraded") {
2469
2480
  await this.#events.emit("cryptoDegraded", event.payload);
2481
+ } else if (event.type === "messageUnavailable") {
2482
+ await this.#events.emit("messageUnavailable", event.payload);
2483
+ } else if (event.type === "messageRecovered") {
2484
+ await this.#events.emit("messageRecovered", event.payload);
2485
+ } else if (event.type === "messageRecoveryFailed") {
2486
+ await this.#events.emit("messageRecoveryFailed", event.payload);
2487
+ } else if (event.type === "messageDecodeFailure") {
2488
+ await this.#events.emit("messageDecodeFailure", event.payload);
2489
+ } else if (event.type === "messageDiscarded") {
2490
+ await this.#events.emit("messageDiscarded", event.payload);
2470
2491
  }
2471
2492
  await this.#refreshHealthState();
2472
2493
  });
@@ -3141,6 +3162,8 @@ var GROUP_METADATA_CACHE_TTL_MS = 18e4;
3141
3162
  var DEVICE_LIST_CACHE_TTL_MS = 18e4;
3142
3163
  var GROUP_METADATA_RECOVERY_COOLDOWN_MS = 6e4;
3143
3164
  var GROUP_METADATA_MISMATCH_WARNING = "group message publish acknowledged with mismatch metadata";
3165
+ var MESSAGE_RECOVERY_TIMEOUT_MS = 3e4;
3166
+ var DECRYPT_CORRELATION_TTL_MS = 15e3;
3144
3167
  var messageSnapshotRetentionSeconds = 7 * 24 * 60 * 60;
3145
3168
  var messageSnapshotMaxPerSession = 2e4;
3146
3169
  var messageSnapshotPruneInterval = 256;
@@ -3151,6 +3174,8 @@ var ZapoProvider = class {
3151
3174
  #messageStore = /* @__PURE__ */ new Map();
3152
3175
  #messageKeyStore = /* @__PURE__ */ new WeakMap();
3153
3176
  #deliveredMessageStore = /* @__PURE__ */ new Set();
3177
+ #pendingUnavailableRecoveries = /* @__PURE__ */ new Map();
3178
+ #recentDecryptedPayloads = /* @__PURE__ */ new Map();
3154
3179
  #handledProtocolStore = /* @__PURE__ */ new Set();
3155
3180
  #callCreatorStore = /* @__PURE__ */ new Map();
3156
3181
  #groupMetadataRecoveryAt = /* @__PURE__ */ new Map();
@@ -3181,6 +3206,17 @@ var ZapoProvider = class {
3181
3206
  #sentMessages = 0;
3182
3207
  #receivedMessages = 0;
3183
3208
  #failedMessages = 0;
3209
+ #decryptedPayloads = 0;
3210
+ #unavailableMessages = 0;
3211
+ #resendRequestedMessages = 0;
3212
+ #recoveredMessages = 0;
3213
+ #recoveryFailedMessages = 0;
3214
+ #unavailableUnrecoverableMessages = 0;
3215
+ #decodeFailures = 0;
3216
+ #unhandledStanzas = 0;
3217
+ #ignoredOfflineMessages = 0;
3218
+ #duplicateMessages = 0;
3219
+ #normalizationFailures = 0;
3184
3220
  #lastIncomingAt;
3185
3221
  #lastOutgoingAt;
3186
3222
  #decryptFailures = 0;
@@ -3221,6 +3257,17 @@ var ZapoProvider = class {
3221
3257
  sent: this.#sentMessages,
3222
3258
  received: this.#receivedMessages,
3223
3259
  failed: this.#failedMessages,
3260
+ decryptedPayloads: this.#decryptedPayloads,
3261
+ unavailable: this.#unavailableMessages,
3262
+ resendRequested: this.#resendRequestedMessages,
3263
+ recovered: this.#recoveredMessages,
3264
+ recoveryFailed: this.#recoveryFailedMessages,
3265
+ unavailableUnrecoverable: this.#unavailableUnrecoverableMessages,
3266
+ decodeFailures: this.#decodeFailures,
3267
+ unhandledStanzas: this.#unhandledStanzas,
3268
+ ignoredOffline: this.#ignoredOfflineMessages,
3269
+ duplicates: this.#duplicateMessages,
3270
+ normalizationFailures: this.#normalizationFailures,
3224
3271
  ...this.#lastIncomingAt ? { lastIncomingAt: new Date(this.#lastIncomingAt) } : {},
3225
3272
  ...this.#lastOutgoingAt ? { lastOutgoingAt: new Date(this.#lastOutgoingAt) } : {}
3226
3273
  },
@@ -3267,6 +3314,11 @@ var ZapoProvider = class {
3267
3314
  clearTimeout(this.#healthRefreshTimer);
3268
3315
  this.#healthRefreshTimer = void 0;
3269
3316
  }
3317
+ for (const pending of this.#pendingUnavailableRecoveries.values()) {
3318
+ clearTimeout(pending.timer);
3319
+ }
3320
+ this.#pendingUnavailableRecoveries.clear();
3321
+ this.#recentDecryptedPayloads.clear();
3270
3322
  const client = this.#client;
3271
3323
  this.#connectPromise = void 0;
3272
3324
  try {
@@ -3628,6 +3680,15 @@ var ZapoProvider = class {
3628
3680
  }
3629
3681
  this.#handleMessage(event);
3630
3682
  });
3683
+ client.on("message_unavailable", (event) => {
3684
+ this.#handleUnavailableMessage(event);
3685
+ });
3686
+ client.on("debug_decrypted_payload", (event) => {
3687
+ this.#handleDecryptedPayload(event);
3688
+ });
3689
+ client.on("debug_unhandled_stanza", (event) => {
3690
+ this.#handleUnhandledStanza(event);
3691
+ });
3631
3692
  client.on("message_send", (event) => {
3632
3693
  if (!event.id || !event.message) return;
3633
3694
  this.#sentMessages += 1;
@@ -3666,7 +3727,16 @@ var ZapoProvider = class {
3666
3727
  if (quoted?.key.id && quoted.message) {
3667
3728
  this.#remember(quoted);
3668
3729
  }
3730
+ const deliveryKey = this.#messageDeliveryKey(stored.key);
3731
+ this.#resolveUnavailableRecovery(deliveryKey, stored.key);
3732
+ const decryptedCorrelationKey = this.#stanzaCorrelationKey(
3733
+ stored.key.remoteJid ?? void 0,
3734
+ stored.key.id ?? void 0
3735
+ );
3736
+ if (decryptedCorrelationKey) this.#recentDecryptedPayloads.delete(decryptedCorrelationKey);
3669
3737
  if (this.#isOfflineMessage(stored)) {
3738
+ this.#ignoredOfflineMessages += 1;
3739
+ this.#emitMessageDiscarded("offline", stored.key);
3670
3740
  this.#logger.debug("Ignored message queued before the current live connection.", {
3671
3741
  messageId: stored.key.id ?? void 0,
3672
3742
  chatId: stored.key.remoteJid ?? void 0,
@@ -3674,8 +3744,9 @@ var ZapoProvider = class {
3674
3744
  });
3675
3745
  return;
3676
3746
  }
3677
- const deliveryKey = this.#messageDeliveryKey(stored.key);
3678
3747
  if (stored.key.id && this.#deliveredMessageStore.has(deliveryKey)) {
3748
+ this.#duplicateMessages += 1;
3749
+ this.#emitMessageDiscarded("duplicate", stored.key);
3679
3750
  this.#logger.debug("Ignored duplicate Zapo message event.", {
3680
3751
  messageId: stored.key.id,
3681
3752
  chatId: stored.key.remoteJid ?? void 0
@@ -3683,7 +3754,15 @@ var ZapoProvider = class {
3683
3754
  return;
3684
3755
  }
3685
3756
  const message = normalizeZapoMessage(event);
3686
- if (!message) return;
3757
+ if (!message) {
3758
+ this.#normalizationFailures += 1;
3759
+ this.#emitMessageDiscarded("normalization_failed", stored.key);
3760
+ this.#logger.warn("Could not normalize incoming Zapo message.", {
3761
+ messageId: stored.key.id ?? void 0,
3762
+ chatId: stored.key.remoteJid ?? void 0
3763
+ });
3764
+ return;
3765
+ }
3687
3766
  if (stored.key.id) this.#rememberDeliveredMessage(deliveryKey);
3688
3767
  this.#receivedMessages += 1;
3689
3768
  this.#lastIncomingAt = /* @__PURE__ */ new Date();
@@ -3696,6 +3775,162 @@ var ZapoProvider = class {
3696
3775
  }
3697
3776
  void this.#events.emit("message", message);
3698
3777
  }
3778
+ #handleUnavailableMessage(event) {
3779
+ this.#unavailableMessages += 1;
3780
+ const occurredAt = /* @__PURE__ */ new Date();
3781
+ const messageId = event.key.id || void 0;
3782
+ const chatId = event.key.remoteJid || void 0;
3783
+ const participantId = event.key.participant ?? event.key.participantAlt ?? void 0;
3784
+ this.#emitStability({
3785
+ type: "messageUnavailable",
3786
+ payload: {
3787
+ kind: event.kind,
3788
+ resendRequested: event.resendRequested,
3789
+ occurredAt,
3790
+ ...messageId ? { messageId } : {},
3791
+ ...chatId ? { chatId } : {},
3792
+ ...participantId ? { participantId } : {}
3793
+ }
3794
+ });
3795
+ if (!event.resendRequested) {
3796
+ this.#unavailableUnrecoverableMessages += 1;
3797
+ const log = event.kind === "other" ? this.#logger.warn.bind(this.#logger) : this.#logger.debug.bind(this.#logger);
3798
+ log("Incoming message is unavailable and was not queued for recovery.", {
3799
+ kind: event.kind,
3800
+ ...messageId ? { messageId } : {},
3801
+ ...chatId ? { chatId } : {},
3802
+ ...participantId ? { participantId } : {}
3803
+ });
3804
+ return;
3805
+ }
3806
+ this.#resendRequestedMessages += 1;
3807
+ const deliveryKey = this.#messageDeliveryKey(event.key);
3808
+ const previous = this.#pendingUnavailableRecoveries.get(deliveryKey);
3809
+ if (previous) clearTimeout(previous.timer);
3810
+ const requestedAt = Date.now();
3811
+ const timer = setTimeout(() => {
3812
+ const pending = this.#pendingUnavailableRecoveries.get(deliveryKey);
3813
+ if (!pending || pending.requestedAt !== requestedAt) return;
3814
+ this.#pendingUnavailableRecoveries.delete(deliveryKey);
3815
+ this.#recoveryFailedMessages += 1;
3816
+ this.#markDegraded();
3817
+ const failedAt = /* @__PURE__ */ new Date();
3818
+ this.#emitStability({
3819
+ type: "messageRecoveryFailed",
3820
+ payload: {
3821
+ failedAt,
3822
+ waitedMs: failedAt.getTime() - pending.requestedAt,
3823
+ ...pending.messageId ? { messageId: pending.messageId } : {},
3824
+ ...pending.chatId ? { chatId: pending.chatId } : {},
3825
+ ...pending.participantId ? { participantId: pending.participantId } : {}
3826
+ }
3827
+ });
3828
+ this.#logger.warn("Unavailable message recovery did not arrive in time.", {
3829
+ ...pending.messageId ? { messageId: pending.messageId } : {},
3830
+ ...pending.chatId ? { chatId: pending.chatId } : {},
3831
+ waitedMs: failedAt.getTime() - pending.requestedAt
3832
+ });
3833
+ }, MESSAGE_RECOVERY_TIMEOUT_MS);
3834
+ this.#pendingUnavailableRecoveries.set(deliveryKey, {
3835
+ requestedAt,
3836
+ timer,
3837
+ ...messageId ? { messageId } : {},
3838
+ ...chatId ? { chatId } : {},
3839
+ ...participantId ? { participantId } : {}
3840
+ });
3841
+ this.#logger.info("Incoming message unavailable; primary-device resend requested.", {
3842
+ ...messageId ? { messageId } : {},
3843
+ ...chatId ? { chatId } : {},
3844
+ ...participantId ? { participantId } : {}
3845
+ });
3846
+ }
3847
+ #resolveUnavailableRecovery(deliveryKey, key) {
3848
+ const pending = this.#pendingUnavailableRecoveries.get(deliveryKey);
3849
+ if (!pending) return;
3850
+ clearTimeout(pending.timer);
3851
+ this.#pendingUnavailableRecoveries.delete(deliveryKey);
3852
+ this.#recoveredMessages += 1;
3853
+ const recoveredAt = /* @__PURE__ */ new Date();
3854
+ const participantId = key.participant ?? key.participantAlt ?? pending.participantId;
3855
+ this.#emitStability({
3856
+ type: "messageRecovered",
3857
+ payload: {
3858
+ recoveredAt,
3859
+ recoveryMs: recoveredAt.getTime() - pending.requestedAt,
3860
+ ...key.id ? { messageId: key.id } : pending.messageId ? { messageId: pending.messageId } : {},
3861
+ ...key.remoteJid ? { chatId: key.remoteJid } : pending.chatId ? { chatId: pending.chatId } : {},
3862
+ ...participantId ? { participantId } : {}
3863
+ }
3864
+ });
3865
+ this.#logger.info("Recovered unavailable message from the primary device.", {
3866
+ ...key.id ? { messageId: key.id } : {},
3867
+ ...key.remoteJid ? { chatId: key.remoteJid } : {},
3868
+ recoveryMs: recoveredAt.getTime() - pending.requestedAt
3869
+ });
3870
+ }
3871
+ #handleDecryptedPayload(event) {
3872
+ this.#decryptedPayloads += 1;
3873
+ const correlationKey = this.#stanzaCorrelationKey(event.chatJid, event.stanzaId);
3874
+ if (!correlationKey) return;
3875
+ const now = Date.now();
3876
+ this.#recentDecryptedPayloads.set(correlationKey, {
3877
+ observedAt: now,
3878
+ encType: event.encType
3879
+ });
3880
+ for (const [key, value] of this.#recentDecryptedPayloads) {
3881
+ if (now - value.observedAt > DECRYPT_CORRELATION_TTL_MS) {
3882
+ this.#recentDecryptedPayloads.delete(key);
3883
+ }
3884
+ }
3885
+ }
3886
+ #handleUnhandledStanza(event) {
3887
+ this.#unhandledStanzas += 1;
3888
+ const correlationKey = this.#stanzaCorrelationKey(event.chatJid, event.stanzaId);
3889
+ const decrypted = correlationKey ? this.#recentDecryptedPayloads.get(correlationKey) : void 0;
3890
+ const now = Date.now();
3891
+ if (decrypted && now - decrypted.observedAt <= DECRYPT_CORRELATION_TTL_MS) {
3892
+ this.#decodeFailures += 1;
3893
+ this.#markDegraded();
3894
+ this.#emitStability({
3895
+ type: "messageDecodeFailure",
3896
+ payload: {
3897
+ occurredAt: new Date(now),
3898
+ reason: event.reason,
3899
+ encType: decrypted.encType,
3900
+ ...event.stanzaId ? { stanzaId: event.stanzaId } : {},
3901
+ ...event.chatJid ? { chatId: event.chatJid } : {}
3902
+ }
3903
+ });
3904
+ this.#logger.warn("Decrypted incoming payload could not be decoded into a supported stanza.", {
3905
+ reason: event.reason,
3906
+ encType: decrypted.encType,
3907
+ ...event.stanzaId ? { stanzaId: event.stanzaId } : {},
3908
+ ...event.chatJid ? { chatId: event.chatJid } : {}
3909
+ });
3910
+ if (correlationKey) this.#recentDecryptedPayloads.delete(correlationKey);
3911
+ return;
3912
+ }
3913
+ this.#logger.debug("Incoming stanza was not handled by Zapo.", {
3914
+ reason: event.reason,
3915
+ ...event.stanzaId ? { stanzaId: event.stanzaId } : {},
3916
+ ...event.chatJid ? { chatId: event.chatJid } : {}
3917
+ });
3918
+ }
3919
+ #emitMessageDiscarded(reason, key) {
3920
+ this.#emitStability({
3921
+ type: "messageDiscarded",
3922
+ payload: {
3923
+ reason,
3924
+ occurredAt: /* @__PURE__ */ new Date(),
3925
+ ...key.id ? { messageId: key.id } : {},
3926
+ ...key.remoteJid ? { chatId: key.remoteJid } : {}
3927
+ }
3928
+ });
3929
+ }
3930
+ #stanzaCorrelationKey(chatJid, stanzaId) {
3931
+ if (!stanzaId) return void 0;
3932
+ return `${chatJid ?? ""}:${stanzaId}`;
3933
+ }
3699
3934
  #handleAddonEvent(event) {
3700
3935
  const decrypted = this.#addonRecord(event.decrypted);
3701
3936
  const protocol = this.#addonProtocolMessage(decrypted);
@@ -4433,11 +4668,16 @@ var ZapoProvider = class {
4433
4668
  return mentions.map((mention) => typeof mention === "string" ? mention : mention.mentionId);
4434
4669
  }
4435
4670
  #toZapoKey(key) {
4671
+ const original = this.#messageKeyStore.get(key)?.key;
4672
+ const participant = original?.participant ?? key.participantId;
4436
4673
  return {
4437
4674
  id: key.id,
4438
4675
  remoteJid: key.chatId,
4439
4676
  fromMe: key.fromMe,
4440
- ...key.participantId ? { participant: key.participantId } : {}
4677
+ ...original?.remoteJidAlt ? { remoteJidAlt: original.remoteJidAlt } : {},
4678
+ ...participant ? { participant } : {},
4679
+ ...original?.participantAlt ? { participantAlt: original.participantAlt } : {},
4680
+ ...original?.addressingMode ? { addressingMode: original.addressingMode } : {}
4441
4681
  };
4442
4682
  }
4443
4683
  #sent(result, chatId) {