@whanext/core 0.19.19 → 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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.19.20
4
+
5
+ ### Ingress diagnostics
6
+ - Added opt-in provider ingress tracing with `providerDiagnostics.ingress`, correlating Zapo transport message stanzas through decrypt and terminal provider events without logging message content or decrypted bytes.
7
+ - Added a bounded ingress watchdog through `providerDiagnostics.ingressStallTimeoutMs`, defaulting to 10 seconds. A decoded `<message>` stanza that never becomes `message`, `message_unavailable`, a decode failure, or a decrypt failure emits `messageIngressStalled` and temporarily marks the provider as degraded.
8
+ - Added transport decode diagnostics from Zapo 1.8.0 `debug_transport_decode_error` with a typed `transportDecodeFailure` event. Only frame size and error metadata are exposed; raw frame bytes are never logged or emitted.
9
+ - Expanded `health().messaging` with `transportFramesIn`, `transportNodesIn`, `transportDecodeErrors`, `messageStanzasIn`, and `ingressStalls`.
10
+ - When ingress diagnostics are enabled, message stanzas log their stanza id, chat, participant, addressing mode, encryption types, decrypt stage, and terminal provider stage. Message text and plaintext payloads are never included.
11
+
12
+ ### Reliability
13
+ - Correlates decrypt warnings with pending message stanzas so known Signal failures do not appear as generic ingress stalls.
14
+ - Normalizes device-qualified PN/LID JIDs before ingress correlation, keeping group and direct-message diagnostics aligned with Zapo message keys.
15
+
3
16
  ## 0.19.19
4
17
 
5
18
  ### Provider
package/README.md CHANGED
@@ -347,6 +347,19 @@ console.log(messaging.duplicates);
347
347
 
348
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.
349
349
 
350
+ Para diagnosticar mensagens que chegam ao socket mas não viram um `Message` público, ative a trilha de ingresso:
351
+
352
+ ```ts
353
+ const app = await create({
354
+ providerDiagnostics: {
355
+ ingress: true,
356
+ ingressStallTimeoutMs: 10_000,
357
+ },
358
+ });
359
+ ```
360
+
361
+ Com essa opção, o provider registra apenas metadados seguros de stanzas `<message>` e correlaciona `debug_transport_node_in`, `debug_decrypted_payload`, `message`, `message_unavailable`, falhas de decrypt e falhas de decode. `health().messaging` também expõe `transportFramesIn`, `transportNodesIn`, `transportDecodeErrors`, `messageStanzasIn` e `ingressStalls`. Frames brutos, bytes descriptografados e texto das mensagens nunca são escritos nos logs.
362
+
350
363
  ```ts
351
364
  server.get('/health', async () => app.health());
352
365
  ```
package/dist/index.d.ts CHANGED
@@ -326,6 +326,11 @@ interface ProviderMessagingHealth {
326
326
  ignoredOffline: number;
327
327
  duplicates: number;
328
328
  normalizationFailures: number;
329
+ transportFramesIn: number;
330
+ transportNodesIn: number;
331
+ transportDecodeErrors: number;
332
+ messageStanzasIn: number;
333
+ ingressStalls: number;
329
334
  lastIncomingAt?: Date;
330
335
  lastOutgoingAt?: Date;
331
336
  }
@@ -401,6 +406,23 @@ interface MessageDiscardedEvent {
401
406
  messageId?: string;
402
407
  chatId?: string;
403
408
  }
409
+ type MessageIngressStage = 'stanza_received' | 'decrypted';
410
+ interface MessageIngressStalledEvent {
411
+ occurredAt: Date;
412
+ waitedMs: number;
413
+ lastStage: MessageIngressStage;
414
+ stanzaId: string;
415
+ chatId?: string;
416
+ participantId?: string;
417
+ stanzaType?: string;
418
+ addressingMode?: string;
419
+ }
420
+ interface TransportDecodeFailureEvent {
421
+ occurredAt: Date;
422
+ frameBytes: number;
423
+ errorName: string;
424
+ errorMessage: string;
425
+ }
404
426
  type ProviderStabilityEvent = {
405
427
  type: 'groupMetadataRecovered';
406
428
  payload: GroupMetadataRecoveredEvent;
@@ -422,6 +444,12 @@ type ProviderStabilityEvent = {
422
444
  } | {
423
445
  type: 'messageDiscarded';
424
446
  payload: MessageDiscardedEvent;
447
+ } | {
448
+ type: 'messageIngressStalled';
449
+ payload: MessageIngressStalledEvent;
450
+ } | {
451
+ type: 'transportDecodeFailure';
452
+ payload: TransportDecodeFailureEvent;
425
453
  } | {
426
454
  type: 'healthRefresh';
427
455
  payload: {
@@ -1003,6 +1031,8 @@ interface AppEvents {
1003
1031
  messageRecoveryFailed: MessageRecoveryFailedEvent;
1004
1032
  messageDecodeFailure: MessageDecodeFailureEvent;
1005
1033
  messageDiscarded: MessageDiscardedEvent;
1034
+ messageIngressStalled: MessageIngressStalledEvent;
1035
+ transportDecodeFailure: TransportDecodeFailureEvent;
1006
1036
  commandQueueTimeout: CommandQueueTimeoutEvent;
1007
1037
  commandQueueFull: CommandQueueFullEvent;
1008
1038
  }
@@ -1068,6 +1098,10 @@ interface ProviderTimeoutOptions {
1068
1098
  connectTimeoutMs?: number;
1069
1099
  nodeQueryTimeoutMs?: number;
1070
1100
  }
1101
+ interface ProviderDiagnosticsOptions {
1102
+ ingress?: boolean;
1103
+ ingressStallTimeoutMs?: number;
1104
+ }
1071
1105
  interface ReconnectOptions {
1072
1106
  enabled?: boolean;
1073
1107
  maxAttempts?: number;
@@ -1085,6 +1119,7 @@ interface CreateOptions {
1085
1119
  router?: Omit<RouterOptions, 'prefix'>;
1086
1120
  reconnect?: ReconnectOptions;
1087
1121
  providerTimeouts?: ProviderTimeoutOptions;
1122
+ providerDiagnostics?: ProviderDiagnosticsOptions;
1088
1123
  messageCacheSize?: number;
1089
1124
  processOfflineMessages?: boolean;
1090
1125
  provider?: WhatsAppProvider;
@@ -1172,4 +1207,4 @@ declare class SqliteMuteStore implements MuteStore {
1172
1207
  close(): void;
1173
1208
  }
1174
1209
 
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 };
1210
+ 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 MessageIngressStage, type MessageIngressStalledEvent, 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 ProviderDiagnosticsOptions, 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, type TransportDecodeFailureEvent, 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
@@ -2357,7 +2357,12 @@ var WhaNextApp = class {
2357
2357
  unhandledStanzas: 0,
2358
2358
  ignoredOffline: 0,
2359
2359
  duplicates: 0,
2360
- normalizationFailures: 0
2360
+ normalizationFailures: 0,
2361
+ transportFramesIn: 0,
2362
+ transportNodesIn: 0,
2363
+ transportDecodeErrors: 0,
2364
+ messageStanzasIn: 0,
2365
+ ingressStalls: 0
2361
2366
  };
2362
2367
  const crypto = provider?.crypto ?? {
2363
2368
  backend: "unknown",
@@ -2488,6 +2493,10 @@ var WhaNextApp = class {
2488
2493
  await this.#events.emit("messageDecodeFailure", event.payload);
2489
2494
  } else if (event.type === "messageDiscarded") {
2490
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);
2491
2500
  }
2492
2501
  await this.#refreshHealthState();
2493
2502
  });
@@ -3157,6 +3166,7 @@ var sharedMediaProcessor = createMediaProcessor();
3157
3166
  var REMOTE_MEDIA_TIMEOUT_MS = 12e4;
3158
3167
  var DEFAULT_CONNECT_TIMEOUT_MS = 15e3;
3159
3168
  var DEFAULT_NODE_QUERY_TIMEOUT_MS = 3e4;
3169
+ var DEFAULT_INGRESS_STALL_TIMEOUT_MS = 1e4;
3160
3170
  var PROVIDER_DEGRADED_WINDOW_MS = 12e4;
3161
3171
  var GROUP_METADATA_CACHE_TTL_MS = 18e4;
3162
3172
  var DEVICE_LIST_CACHE_TTL_MS = 18e4;
@@ -3176,6 +3186,7 @@ var ZapoProvider = class {
3176
3186
  #deliveredMessageStore = /* @__PURE__ */ new Set();
3177
3187
  #pendingUnavailableRecoveries = /* @__PURE__ */ new Map();
3178
3188
  #recentDecryptedPayloads = /* @__PURE__ */ new Map();
3189
+ #pendingIngressMessages = /* @__PURE__ */ new Map();
3179
3190
  #handledProtocolStore = /* @__PURE__ */ new Set();
3180
3191
  #callCreatorStore = /* @__PURE__ */ new Map();
3181
3192
  #groupMetadataRecoveryAt = /* @__PURE__ */ new Map();
@@ -3184,6 +3195,8 @@ var ZapoProvider = class {
3184
3195
  #cryptoBackend;
3185
3196
  #connectTimeoutMs;
3186
3197
  #nodeQueryTimeoutMs;
3198
+ #ingressDiagnostics;
3199
+ #ingressStallTimeoutMs;
3187
3200
  #protocolMutationQueue = Promise.resolve();
3188
3201
  #state = "idle";
3189
3202
  #client;
@@ -3217,6 +3230,11 @@ var ZapoProvider = class {
3217
3230
  #ignoredOfflineMessages = 0;
3218
3231
  #duplicateMessages = 0;
3219
3232
  #normalizationFailures = 0;
3233
+ #transportFramesIn = 0;
3234
+ #transportNodesIn = 0;
3235
+ #transportDecodeErrors = 0;
3236
+ #messageStanzasIn = 0;
3237
+ #ingressStalls = 0;
3220
3238
  #lastIncomingAt;
3221
3239
  #lastOutgoingAt;
3222
3240
  #decryptFailures = 0;
@@ -3234,6 +3252,8 @@ var ZapoProvider = class {
3234
3252
  this.#messageCacheSize = Math.max(1, options.messageCacheSize ?? 1e3);
3235
3253
  this.#connectTimeoutMs = normalizeProviderTimeout(options.connectTimeoutMs, DEFAULT_CONNECT_TIMEOUT_MS);
3236
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);
3237
3257
  this.#cryptoBackend = detectCryptoBackend();
3238
3258
  }
3239
3259
  on(event, listener) {
@@ -3268,6 +3288,11 @@ var ZapoProvider = class {
3268
3288
  ignoredOffline: this.#ignoredOfflineMessages,
3269
3289
  duplicates: this.#duplicateMessages,
3270
3290
  normalizationFailures: this.#normalizationFailures,
3291
+ transportFramesIn: this.#transportFramesIn,
3292
+ transportNodesIn: this.#transportNodesIn,
3293
+ transportDecodeErrors: this.#transportDecodeErrors,
3294
+ messageStanzasIn: this.#messageStanzasIn,
3295
+ ingressStalls: this.#ingressStalls,
3271
3296
  ...this.#lastIncomingAt ? { lastIncomingAt: new Date(this.#lastIncomingAt) } : {},
3272
3297
  ...this.#lastOutgoingAt ? { lastOutgoingAt: new Date(this.#lastOutgoingAt) } : {}
3273
3298
  },
@@ -3318,6 +3343,10 @@ var ZapoProvider = class {
3318
3343
  clearTimeout(pending.timer);
3319
3344
  }
3320
3345
  this.#pendingUnavailableRecoveries.clear();
3346
+ for (const pending of this.#pendingIngressMessages.values()) {
3347
+ clearTimeout(pending.timer);
3348
+ }
3349
+ this.#pendingIngressMessages.clear();
3321
3350
  this.#recentDecryptedPayloads.clear();
3322
3351
  const client = this.#client;
3323
3352
  this.#connectPromise = void 0;
@@ -3657,7 +3686,22 @@ var ZapoProvider = class {
3657
3686
  );
3658
3687
  void this.#stopTerminalConnection(client, error);
3659
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
+ });
3660
3703
  client.on("message", (event) => {
3704
+ this.#resolveIngressMessage(event.key, "message_event");
3661
3705
  const protocol = this.#protocolMessage(event);
3662
3706
  if (protocol && this.#isMessageMutationProtocol(protocol.type)) {
3663
3707
  this.#enqueueProtocolEvent({
@@ -3681,6 +3725,7 @@ var ZapoProvider = class {
3681
3725
  this.#handleMessage(event);
3682
3726
  });
3683
3727
  client.on("message_unavailable", (event) => {
3728
+ this.#resolveIngressMessage(event.key, "unavailable");
3684
3729
  this.#handleUnavailableMessage(event);
3685
3730
  });
3686
3731
  client.on("debug_decrypted_payload", (event) => {
@@ -3720,6 +3765,139 @@ var ZapoProvider = class {
3720
3765
  void this.#handleConnectionEvent(client, event);
3721
3766
  });
3722
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
+ }
3723
3901
  #handleMessage(event) {
3724
3902
  const stored = event;
3725
3903
  if (stored.key?.id && stored.message) this.#remember(stored);
@@ -3730,7 +3908,7 @@ var ZapoProvider = class {
3730
3908
  const deliveryKey = this.#messageDeliveryKey(stored.key);
3731
3909
  this.#resolveUnavailableRecovery(deliveryKey, stored.key);
3732
3910
  const decryptedCorrelationKey = this.#stanzaCorrelationKey(
3733
- stored.key.remoteJid ?? void 0,
3911
+ normalizeIngressJid(stored.key.remoteJid ?? void 0),
3734
3912
  stored.key.id ?? void 0
3735
3913
  );
3736
3914
  if (decryptedCorrelationKey) this.#recentDecryptedPayloads.delete(decryptedCorrelationKey);
@@ -3766,6 +3944,16 @@ var ZapoProvider = class {
3766
3944
  if (stored.key.id) this.#rememberDeliveredMessage(deliveryKey);
3767
3945
  this.#receivedMessages += 1;
3768
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
+ }
3769
3957
  this.#messageKeyStore.set(message.keys, stored);
3770
3958
  if (message.quoted && quoted?.message) {
3771
3959
  this.#messageKeyStore.set(
@@ -3870,7 +4058,8 @@ var ZapoProvider = class {
3870
4058
  }
3871
4059
  #handleDecryptedPayload(event) {
3872
4060
  this.#decryptedPayloads += 1;
3873
- const correlationKey = this.#stanzaCorrelationKey(event.chatJid, event.stanzaId);
4061
+ this.#markIngressDecrypted(event.chatJid, event.stanzaId);
4062
+ const correlationKey = this.#stanzaCorrelationKey(normalizeIngressJid(event.chatJid), event.stanzaId);
3874
4063
  if (!correlationKey) return;
3875
4064
  const now = Date.now();
3876
4065
  this.#recentDecryptedPayloads.set(correlationKey, {
@@ -3885,11 +4074,13 @@ var ZapoProvider = class {
3885
4074
  }
3886
4075
  #handleUnhandledStanza(event) {
3887
4076
  this.#unhandledStanzas += 1;
3888
- const correlationKey = this.#stanzaCorrelationKey(event.chatJid, event.stanzaId);
4077
+ const normalizedChatId = normalizeIngressJid(event.chatJid);
4078
+ const correlationKey = this.#stanzaCorrelationKey(normalizedChatId, event.stanzaId);
3889
4079
  const decrypted = correlationKey ? this.#recentDecryptedPayloads.get(correlationKey) : void 0;
3890
4080
  const now = Date.now();
3891
4081
  if (decrypted && now - decrypted.observedAt <= DECRYPT_CORRELATION_TTL_MS) {
3892
4082
  this.#decodeFailures += 1;
4083
+ this.#resolveIngressByStanza(normalizedChatId, event.stanzaId, "decode_failure");
3893
4084
  this.#markDegraded();
3894
4085
  this.#emitStability({
3895
4086
  type: "messageDecodeFailure",
@@ -3910,6 +4101,7 @@ var ZapoProvider = class {
3910
4101
  if (correlationKey) this.#recentDecryptedPayloads.delete(correlationKey);
3911
4102
  return;
3912
4103
  }
4104
+ this.#resolveIngressByStanza(normalizedChatId, event.stanzaId, "decode_failure");
3913
4105
  this.#logger.debug("Incoming stanza was not handled by Zapo.", {
3914
4106
  reason: event.reason,
3915
4107
  ...event.stanzaId ? { stanzaId: event.stanzaId } : {},
@@ -4188,6 +4380,11 @@ var ZapoProvider = class {
4188
4380
  }
4189
4381
  if (message === "failed to decrypt incoming message") {
4190
4382
  this.#decryptFailures += 1;
4383
+ this.#resolveIngressByStanza(
4384
+ stringValue(context.from) ?? stringValue(context.groupJid),
4385
+ stringValue(context.id),
4386
+ "decrypt_failure"
4387
+ );
4191
4388
  const detail = stringValue(context.message);
4192
4389
  const kind = detail === "sender key id mismatch" ? "sender_key_mismatch" : "decrypt_failure";
4193
4390
  if (kind === "sender_key_mismatch") this.#senderKeyMismatches += 1;
@@ -5353,6 +5550,13 @@ function normalizeProviderTimeout(value, fallback) {
5353
5550
  if (value === void 0 || !Number.isFinite(value)) return fallback;
5354
5551
  return Math.max(1e3, Math.floor(value));
5355
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
+ }
5356
5560
  function detectCryptoBackend() {
5357
5561
  const requested = process.env.ZAPO_NATIVE_BACKEND?.trim().toLowerCase() ?? "auto";
5358
5562
  if (requested === "js" || requested === "none") return "js";
@@ -5453,7 +5657,9 @@ async function create(options = {}) {
5453
5657
  ...options.processOfflineMessages !== void 0 ? { processOfflineMessages: options.processOfflineMessages } : {},
5454
5658
  ...options.reconnect ? { reconnect: options.reconnect } : {},
5455
5659
  ...options.providerTimeouts?.connectTimeoutMs !== void 0 ? { connectTimeoutMs: options.providerTimeouts.connectTimeoutMs } : {},
5456
- ...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 } : {}
5457
5663
  });
5458
5664
  return new WhaNextApp(provider, {
5459
5665
  ...options.accountId ? { accountId: options.accountId } : {},
@@ -5691,6 +5897,12 @@ function mergeCreateOptions(shared, account) {
5691
5897
  ...account.providerTimeouts
5692
5898
  };
5693
5899
  }
5900
+ if (shared.providerDiagnostics || account.providerDiagnostics) {
5901
+ merged.providerDiagnostics = {
5902
+ ...shared.providerDiagnostics,
5903
+ ...account.providerDiagnostics
5904
+ };
5905
+ }
5694
5906
  if (shared.logger && account.logger && typeof shared.logger === "object" && typeof account.logger === "object") {
5695
5907
  merged.logger = {
5696
5908
  ...shared.logger,