@whanext/core 0.19.19 → 0.19.21

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,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.19.21
4
+
5
+ ### Added
6
+ - `MessageService.edit()` accepts an optional protocol-level `id` override for the outgoing edit envelope.
7
+
8
+ ### Zapo
9
+ - Forwards the optional edit `id` to `WaSendMessageOptions.id`, enabling specialized moderation workflows without exposing the provider client.
10
+
11
+ ## 0.19.20
12
+
13
+ ### Ingress diagnostics
14
+ - 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.
15
+ - 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.
16
+ - 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.
17
+ - Expanded `health().messaging` with `transportFramesIn`, `transportNodesIn`, `transportDecodeErrors`, `messageStanzasIn`, and `ingressStalls`.
18
+ - 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.
19
+
20
+ ### Reliability
21
+ - Correlates decrypt warnings with pending message stanzas so known Signal failures do not appear as generic ingress stalls.
22
+ - Normalizes device-qualified PN/LID JIDs before ingress correlation, keeping group and direct-message diagnostics aligned with Zapo message keys.
23
+
3
24
  ## 0.19.19
4
25
 
5
26
  ### 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
@@ -163,6 +163,13 @@ type MentionTarget = string | User;
163
163
  interface RepostMessageOptions {
164
164
  mentions?: readonly MentionTarget[];
165
165
  }
166
+ interface EditMessageOptions {
167
+ /**
168
+ * Overrides the stanza id used by the outgoing edit envelope.
169
+ * Advanced/protocol-level option; omitted for normal edits.
170
+ */
171
+ id?: string;
172
+ }
166
173
  interface TextContent {
167
174
  text: string;
168
175
  mentions?: MentionTarget[];
@@ -326,6 +333,11 @@ interface ProviderMessagingHealth {
326
333
  ignoredOffline: number;
327
334
  duplicates: number;
328
335
  normalizationFailures: number;
336
+ transportFramesIn: number;
337
+ transportNodesIn: number;
338
+ transportDecodeErrors: number;
339
+ messageStanzasIn: number;
340
+ ingressStalls: number;
329
341
  lastIncomingAt?: Date;
330
342
  lastOutgoingAt?: Date;
331
343
  }
@@ -401,6 +413,23 @@ interface MessageDiscardedEvent {
401
413
  messageId?: string;
402
414
  chatId?: string;
403
415
  }
416
+ type MessageIngressStage = 'stanza_received' | 'decrypted';
417
+ interface MessageIngressStalledEvent {
418
+ occurredAt: Date;
419
+ waitedMs: number;
420
+ lastStage: MessageIngressStage;
421
+ stanzaId: string;
422
+ chatId?: string;
423
+ participantId?: string;
424
+ stanzaType?: string;
425
+ addressingMode?: string;
426
+ }
427
+ interface TransportDecodeFailureEvent {
428
+ occurredAt: Date;
429
+ frameBytes: number;
430
+ errorName: string;
431
+ errorMessage: string;
432
+ }
404
433
  type ProviderStabilityEvent = {
405
434
  type: 'groupMetadataRecovered';
406
435
  payload: GroupMetadataRecoveredEvent;
@@ -422,6 +451,12 @@ type ProviderStabilityEvent = {
422
451
  } | {
423
452
  type: 'messageDiscarded';
424
453
  payload: MessageDiscardedEvent;
454
+ } | {
455
+ type: 'messageIngressStalled';
456
+ payload: MessageIngressStalledEvent;
457
+ } | {
458
+ type: 'transportDecodeFailure';
459
+ payload: TransportDecodeFailureEvent;
425
460
  } | {
426
461
  type: 'healthRefresh';
427
462
  payload: {
@@ -452,7 +487,7 @@ interface WhatsAppProvider {
452
487
  repostMessage(source: MessageKey, chatId: string, options?: RepostMessageOptions): Promise<SentMessage>;
453
488
  reactToMessage(key: MessageKey, emoji?: string): Promise<SentMessage>;
454
489
  downloadMedia(key: MessageKey): Promise<DownloadedMedia>;
455
- editMessage(key: MessageKey, content: string): Promise<SentMessage>;
490
+ editMessage(key: MessageKey, content: string, options?: EditMessageOptions): Promise<SentMessage>;
456
491
  deleteMessage(key: MessageKey): Promise<void>;
457
492
  getGroup(groupId: string): Promise<GroupSnapshot>;
458
493
  setGroupAccess(groupId: string, access: GroupAccess): Promise<void>;
@@ -658,7 +693,7 @@ declare class MessageService {
658
693
  * media and structured WhatsApp messages as well as text.
659
694
  */
660
695
  repost(source: Message | MessageKey, chatId: string, options?: RepostMessageOptions): Promise<SentMessage>;
661
- edit(message: Message | SentMessage | MessageKey, text: string): Promise<SentMessage>;
696
+ edit(message: Message | SentMessage | MessageKey, text: string, options?: EditMessageOptions): Promise<SentMessage>;
662
697
  delete(message: Message | SentMessage | MessageKey): Promise<void>;
663
698
  react(message: Message | SentMessage | MessageKey, emoji: string): Promise<SentMessage>;
664
699
  unreact(message: Message | SentMessage | MessageKey): Promise<SentMessage>;
@@ -1003,6 +1038,8 @@ interface AppEvents {
1003
1038
  messageRecoveryFailed: MessageRecoveryFailedEvent;
1004
1039
  messageDecodeFailure: MessageDecodeFailureEvent;
1005
1040
  messageDiscarded: MessageDiscardedEvent;
1041
+ messageIngressStalled: MessageIngressStalledEvent;
1042
+ transportDecodeFailure: TransportDecodeFailureEvent;
1006
1043
  commandQueueTimeout: CommandQueueTimeoutEvent;
1007
1044
  commandQueueFull: CommandQueueFullEvent;
1008
1045
  }
@@ -1068,6 +1105,10 @@ interface ProviderTimeoutOptions {
1068
1105
  connectTimeoutMs?: number;
1069
1106
  nodeQueryTimeoutMs?: number;
1070
1107
  }
1108
+ interface ProviderDiagnosticsOptions {
1109
+ ingress?: boolean;
1110
+ ingressStallTimeoutMs?: number;
1111
+ }
1071
1112
  interface ReconnectOptions {
1072
1113
  enabled?: boolean;
1073
1114
  maxAttempts?: number;
@@ -1085,6 +1126,7 @@ interface CreateOptions {
1085
1126
  router?: Omit<RouterOptions, 'prefix'>;
1086
1127
  reconnect?: ReconnectOptions;
1087
1128
  providerTimeouts?: ProviderTimeoutOptions;
1129
+ providerDiagnostics?: ProviderDiagnosticsOptions;
1088
1130
  messageCacheSize?: number;
1089
1131
  processOfflineMessages?: boolean;
1090
1132
  provider?: WhatsAppProvider;
@@ -1172,4 +1214,4 @@ declare class SqliteMuteStore implements MuteStore {
1172
1214
  close(): void;
1173
1215
  }
1174
1216
 
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 };
1217
+ 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 EditMessageOptions, 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
@@ -2239,9 +2239,9 @@ var MessageService = class {
2239
2239
  const key = "keys" in source ? source.keys : source;
2240
2240
  return this.#provider.repostMessage(key, chatId, options);
2241
2241
  }
2242
- edit(message, text) {
2242
+ edit(message, text, options = {}) {
2243
2243
  const key = "keys" in message ? message.keys : message;
2244
- return this.#provider.editMessage(key, text);
2244
+ return this.#provider.editMessage(key, text, options);
2245
2245
  }
2246
2246
  delete(message) {
2247
2247
  const key = "keys" in message ? message.keys : message;
@@ -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;
@@ -3462,12 +3491,15 @@ var ZapoProvider = class {
3462
3491
  );
3463
3492
  }
3464
3493
  }
3465
- async editMessage(key, content) {
3494
+ async editMessage(key, content, options = {}) {
3466
3495
  return this.#trackOutgoing(async () => {
3467
3496
  const result = await this.#requireClient().message.send(
3468
3497
  key.chatId,
3469
3498
  content,
3470
- { editKey: this.#toZapoKey(key) }
3499
+ {
3500
+ editKey: this.#toZapoKey(key),
3501
+ ...options.id ? { id: options.id } : {}
3502
+ }
3471
3503
  );
3472
3504
  return this.#sent(result, key.chatId);
3473
3505
  });
@@ -3657,7 +3689,22 @@ var ZapoProvider = class {
3657
3689
  );
3658
3690
  void this.#stopTerminalConnection(client, error);
3659
3691
  });
3692
+ client.on("debug_transport_frame_in", ({ frame }) => {
3693
+ this.#transportFramesIn += 1;
3694
+ if (this.#ingressDiagnostics) {
3695
+ this.#logger.debug("Inbound WhatsApp transport frame received.", {
3696
+ frameBytes: frame.byteLength
3697
+ });
3698
+ }
3699
+ });
3700
+ client.on("debug_transport_node_in", ({ node }) => {
3701
+ this.#handleTransportNodeIn(node);
3702
+ });
3703
+ client.on("debug_transport_decode_error", ({ error, frame }) => {
3704
+ this.#handleTransportDecodeError(error, frame.byteLength);
3705
+ });
3660
3706
  client.on("message", (event) => {
3707
+ this.#resolveIngressMessage(event.key, "message_event");
3661
3708
  const protocol = this.#protocolMessage(event);
3662
3709
  if (protocol && this.#isMessageMutationProtocol(protocol.type)) {
3663
3710
  this.#enqueueProtocolEvent({
@@ -3681,6 +3728,7 @@ var ZapoProvider = class {
3681
3728
  this.#handleMessage(event);
3682
3729
  });
3683
3730
  client.on("message_unavailable", (event) => {
3731
+ this.#resolveIngressMessage(event.key, "unavailable");
3684
3732
  this.#handleUnavailableMessage(event);
3685
3733
  });
3686
3734
  client.on("debug_decrypted_payload", (event) => {
@@ -3720,6 +3768,139 @@ var ZapoProvider = class {
3720
3768
  void this.#handleConnectionEvent(client, event);
3721
3769
  });
3722
3770
  }
3771
+ #handleTransportNodeIn(node) {
3772
+ this.#transportNodesIn += 1;
3773
+ if (node.tag !== "message") return;
3774
+ this.#messageStanzasIn += 1;
3775
+ const stanzaId = stringValue(node.attrs.id);
3776
+ const chatId = normalizeIngressJid(stringValue(node.attrs.from));
3777
+ const participantId = normalizeIngressJid(
3778
+ stringValue(node.attrs.participant) ?? stringValue(node.attrs.participant_pn) ?? stringValue(node.attrs.participant_lid) ?? stringValue(node.attrs.sender_pn) ?? stringValue(node.attrs.sender_lid)
3779
+ );
3780
+ const stanzaType = stringValue(node.attrs.type);
3781
+ const addressingMode = stringValue(node.attrs.addressing_mode);
3782
+ const children = Array.isArray(node.content) ? node.content : [];
3783
+ const childTags = [...new Set(children.map((child) => child.tag))];
3784
+ const encTypes = [...new Set(children.filter((child) => child.tag === "enc").map((child) => stringValue(child.attrs.type)).filter((value) => value !== void 0))];
3785
+ if (this.#ingressDiagnostics) {
3786
+ this.#logger.info("Inbound WhatsApp message stanza observed.", {
3787
+ ...stanzaId ? { stanzaId } : {},
3788
+ ...chatId ? { chatId } : {},
3789
+ ...participantId ? { participantId } : {},
3790
+ ...stanzaType ? { stanzaType } : {},
3791
+ ...addressingMode ? { addressingMode } : {},
3792
+ ...childTags.length > 0 ? { childTags } : {},
3793
+ ...encTypes.length > 0 ? { encTypes } : {}
3794
+ });
3795
+ }
3796
+ const correlationKey = this.#stanzaCorrelationKey(chatId, stanzaId);
3797
+ if (!correlationKey || !stanzaId) return;
3798
+ const previous = this.#pendingIngressMessages.get(correlationKey);
3799
+ if (previous) clearTimeout(previous.timer);
3800
+ const observedAt = Date.now();
3801
+ const timer = setTimeout(() => {
3802
+ const pending = this.#pendingIngressMessages.get(correlationKey);
3803
+ if (!pending || pending.observedAt !== observedAt) return;
3804
+ this.#pendingIngressMessages.delete(correlationKey);
3805
+ this.#ingressStalls += 1;
3806
+ this.#markDegraded();
3807
+ const occurredAt = /* @__PURE__ */ new Date();
3808
+ this.#emitStability({
3809
+ type: "messageIngressStalled",
3810
+ payload: {
3811
+ occurredAt,
3812
+ waitedMs: occurredAt.getTime() - pending.observedAt,
3813
+ lastStage: pending.lastStage,
3814
+ stanzaId: pending.stanzaId,
3815
+ ...pending.chatId ? { chatId: pending.chatId } : {},
3816
+ ...pending.participantId ? { participantId: pending.participantId } : {},
3817
+ ...pending.stanzaType ? { stanzaType: pending.stanzaType } : {},
3818
+ ...pending.addressingMode ? { addressingMode: pending.addressingMode } : {}
3819
+ }
3820
+ });
3821
+ this.#logger.warn("Inbound WhatsApp message stanza did not reach a terminal provider event.", {
3822
+ stanzaId: pending.stanzaId,
3823
+ ...pending.chatId ? { chatId: pending.chatId } : {},
3824
+ ...pending.participantId ? { participantId: pending.participantId } : {},
3825
+ lastStage: pending.lastStage,
3826
+ waitedMs: occurredAt.getTime() - pending.observedAt
3827
+ });
3828
+ }, this.#ingressStallTimeoutMs);
3829
+ this.#pendingIngressMessages.set(correlationKey, {
3830
+ observedAt,
3831
+ lastStage: "stanza_received",
3832
+ timer,
3833
+ stanzaId,
3834
+ ...chatId ? { chatId } : {},
3835
+ ...participantId ? { participantId } : {},
3836
+ ...stanzaType ? { stanzaType } : {},
3837
+ ...addressingMode ? { addressingMode } : {}
3838
+ });
3839
+ }
3840
+ #handleTransportDecodeError(error, frameBytes) {
3841
+ this.#transportDecodeErrors += 1;
3842
+ this.#markDegraded();
3843
+ const occurredAt = /* @__PURE__ */ new Date();
3844
+ this.#emitStability({
3845
+ type: "transportDecodeFailure",
3846
+ payload: {
3847
+ occurredAt,
3848
+ frameBytes,
3849
+ errorName: error.name || "Error",
3850
+ errorMessage: error.message || String(error)
3851
+ }
3852
+ });
3853
+ this.#logger.warn("Inbound WhatsApp transport frame could not be decoded.", {
3854
+ frameBytes,
3855
+ errorName: error.name || "Error",
3856
+ errorMessage: error.message || String(error)
3857
+ });
3858
+ }
3859
+ #markIngressDecrypted(chatId, stanzaId) {
3860
+ const correlationKey = this.#stanzaCorrelationKey(normalizeIngressJid(chatId), stanzaId);
3861
+ if (!correlationKey) return;
3862
+ const pending = this.#pendingIngressMessages.get(correlationKey);
3863
+ if (!pending) return;
3864
+ pending.lastStage = "decrypted";
3865
+ if (this.#ingressDiagnostics) {
3866
+ this.#logger.info("Inbound WhatsApp message payload decrypted.", {
3867
+ stanzaId: pending.stanzaId,
3868
+ ...pending.chatId ? { chatId: pending.chatId } : {},
3869
+ ...pending.participantId ? { participantId: pending.participantId } : {}
3870
+ });
3871
+ }
3872
+ }
3873
+ #resolveIngressMessage(key, outcome) {
3874
+ const chatId = normalizeIngressJid(key.remoteJid ?? void 0);
3875
+ const correlationKey = this.#stanzaCorrelationKey(chatId, key.id ?? void 0);
3876
+ if (!correlationKey) return;
3877
+ const pending = this.#pendingIngressMessages.get(correlationKey);
3878
+ if (!pending) return;
3879
+ clearTimeout(pending.timer);
3880
+ this.#pendingIngressMessages.delete(correlationKey);
3881
+ if (this.#ingressDiagnostics) {
3882
+ this.#logger.info("Inbound WhatsApp message stanza reached provider terminal stage.", {
3883
+ stanzaId: pending.stanzaId,
3884
+ ...pending.chatId ? { chatId: pending.chatId } : {},
3885
+ ...pending.participantId ? { participantId: pending.participantId } : {},
3886
+ lastStage: pending.lastStage,
3887
+ outcome,
3888
+ durationMs: Date.now() - pending.observedAt
3889
+ });
3890
+ }
3891
+ }
3892
+ #resolveIngressByStanza(chatId, stanzaId, outcome) {
3893
+ const normalizedChatId = normalizeIngressJid(chatId);
3894
+ const correlationKey = this.#stanzaCorrelationKey(normalizedChatId, stanzaId);
3895
+ if (!correlationKey) return;
3896
+ const pending = this.#pendingIngressMessages.get(correlationKey);
3897
+ if (!pending) return;
3898
+ const remoteJid = pending.chatId ?? normalizedChatId;
3899
+ this.#resolveIngressMessage({
3900
+ id: pending.stanzaId,
3901
+ ...remoteJid ? { remoteJid } : {}
3902
+ }, outcome);
3903
+ }
3723
3904
  #handleMessage(event) {
3724
3905
  const stored = event;
3725
3906
  if (stored.key?.id && stored.message) this.#remember(stored);
@@ -3730,7 +3911,7 @@ var ZapoProvider = class {
3730
3911
  const deliveryKey = this.#messageDeliveryKey(stored.key);
3731
3912
  this.#resolveUnavailableRecovery(deliveryKey, stored.key);
3732
3913
  const decryptedCorrelationKey = this.#stanzaCorrelationKey(
3733
- stored.key.remoteJid ?? void 0,
3914
+ normalizeIngressJid(stored.key.remoteJid ?? void 0),
3734
3915
  stored.key.id ?? void 0
3735
3916
  );
3736
3917
  if (decryptedCorrelationKey) this.#recentDecryptedPayloads.delete(decryptedCorrelationKey);
@@ -3766,6 +3947,16 @@ var ZapoProvider = class {
3766
3947
  if (stored.key.id) this.#rememberDeliveredMessage(deliveryKey);
3767
3948
  this.#receivedMessages += 1;
3768
3949
  this.#lastIncomingAt = /* @__PURE__ */ new Date();
3950
+ if (this.#ingressDiagnostics) {
3951
+ this.#logger.info("Inbound WhatsApp message emitted to WhaNext.", {
3952
+ messageId: message.id,
3953
+ chatId: message.chatId,
3954
+ userId: message.sender.id,
3955
+ contentKind: message.contentKind,
3956
+ hasMedia: message.media !== void 0,
3957
+ hasQuoted: message.quoted !== void 0
3958
+ });
3959
+ }
3769
3960
  this.#messageKeyStore.set(message.keys, stored);
3770
3961
  if (message.quoted && quoted?.message) {
3771
3962
  this.#messageKeyStore.set(
@@ -3870,7 +4061,8 @@ var ZapoProvider = class {
3870
4061
  }
3871
4062
  #handleDecryptedPayload(event) {
3872
4063
  this.#decryptedPayloads += 1;
3873
- const correlationKey = this.#stanzaCorrelationKey(event.chatJid, event.stanzaId);
4064
+ this.#markIngressDecrypted(event.chatJid, event.stanzaId);
4065
+ const correlationKey = this.#stanzaCorrelationKey(normalizeIngressJid(event.chatJid), event.stanzaId);
3874
4066
  if (!correlationKey) return;
3875
4067
  const now = Date.now();
3876
4068
  this.#recentDecryptedPayloads.set(correlationKey, {
@@ -3885,11 +4077,13 @@ var ZapoProvider = class {
3885
4077
  }
3886
4078
  #handleUnhandledStanza(event) {
3887
4079
  this.#unhandledStanzas += 1;
3888
- const correlationKey = this.#stanzaCorrelationKey(event.chatJid, event.stanzaId);
4080
+ const normalizedChatId = normalizeIngressJid(event.chatJid);
4081
+ const correlationKey = this.#stanzaCorrelationKey(normalizedChatId, event.stanzaId);
3889
4082
  const decrypted = correlationKey ? this.#recentDecryptedPayloads.get(correlationKey) : void 0;
3890
4083
  const now = Date.now();
3891
4084
  if (decrypted && now - decrypted.observedAt <= DECRYPT_CORRELATION_TTL_MS) {
3892
4085
  this.#decodeFailures += 1;
4086
+ this.#resolveIngressByStanza(normalizedChatId, event.stanzaId, "decode_failure");
3893
4087
  this.#markDegraded();
3894
4088
  this.#emitStability({
3895
4089
  type: "messageDecodeFailure",
@@ -3910,6 +4104,7 @@ var ZapoProvider = class {
3910
4104
  if (correlationKey) this.#recentDecryptedPayloads.delete(correlationKey);
3911
4105
  return;
3912
4106
  }
4107
+ this.#resolveIngressByStanza(normalizedChatId, event.stanzaId, "decode_failure");
3913
4108
  this.#logger.debug("Incoming stanza was not handled by Zapo.", {
3914
4109
  reason: event.reason,
3915
4110
  ...event.stanzaId ? { stanzaId: event.stanzaId } : {},
@@ -4188,6 +4383,11 @@ var ZapoProvider = class {
4188
4383
  }
4189
4384
  if (message === "failed to decrypt incoming message") {
4190
4385
  this.#decryptFailures += 1;
4386
+ this.#resolveIngressByStanza(
4387
+ stringValue(context.from) ?? stringValue(context.groupJid),
4388
+ stringValue(context.id),
4389
+ "decrypt_failure"
4390
+ );
4191
4391
  const detail = stringValue(context.message);
4192
4392
  const kind = detail === "sender key id mismatch" ? "sender_key_mismatch" : "decrypt_failure";
4193
4393
  if (kind === "sender_key_mismatch") this.#senderKeyMismatches += 1;
@@ -5353,6 +5553,13 @@ function normalizeProviderTimeout(value, fallback) {
5353
5553
  if (value === void 0 || !Number.isFinite(value)) return fallback;
5354
5554
  return Math.max(1e3, Math.floor(value));
5355
5555
  }
5556
+ function normalizeIngressJid(value) {
5557
+ if (!value) return void 0;
5558
+ const at = value.indexOf("@");
5559
+ if (at <= 0) return value;
5560
+ const local = value.slice(0, at).replace(/:\d+$/, "");
5561
+ return `${local}${value.slice(at)}`;
5562
+ }
5356
5563
  function detectCryptoBackend() {
5357
5564
  const requested = process.env.ZAPO_NATIVE_BACKEND?.trim().toLowerCase() ?? "auto";
5358
5565
  if (requested === "js" || requested === "none") return "js";
@@ -5453,7 +5660,9 @@ async function create(options = {}) {
5453
5660
  ...options.processOfflineMessages !== void 0 ? { processOfflineMessages: options.processOfflineMessages } : {},
5454
5661
  ...options.reconnect ? { reconnect: options.reconnect } : {},
5455
5662
  ...options.providerTimeouts?.connectTimeoutMs !== void 0 ? { connectTimeoutMs: options.providerTimeouts.connectTimeoutMs } : {},
5456
- ...options.providerTimeouts?.nodeQueryTimeoutMs !== void 0 ? { nodeQueryTimeoutMs: options.providerTimeouts.nodeQueryTimeoutMs } : {}
5663
+ ...options.providerTimeouts?.nodeQueryTimeoutMs !== void 0 ? { nodeQueryTimeoutMs: options.providerTimeouts.nodeQueryTimeoutMs } : {},
5664
+ ...options.providerDiagnostics?.ingress !== void 0 ? { ingressDiagnostics: options.providerDiagnostics.ingress } : {},
5665
+ ...options.providerDiagnostics?.ingressStallTimeoutMs !== void 0 ? { ingressStallTimeoutMs: options.providerDiagnostics.ingressStallTimeoutMs } : {}
5457
5666
  });
5458
5667
  return new WhaNextApp(provider, {
5459
5668
  ...options.accountId ? { accountId: options.accountId } : {},
@@ -5691,6 +5900,12 @@ function mergeCreateOptions(shared, account) {
5691
5900
  ...account.providerTimeouts
5692
5901
  };
5693
5902
  }
5903
+ if (shared.providerDiagnostics || account.providerDiagnostics) {
5904
+ merged.providerDiagnostics = {
5905
+ ...shared.providerDiagnostics,
5906
+ ...account.providerDiagnostics
5907
+ };
5908
+ }
5694
5909
  if (shared.logger && account.logger && typeof shared.logger === "object" && typeof account.logger === "object") {
5695
5910
  merged.logger = {
5696
5911
  ...shared.logger,