@whanext/core 0.19.14 → 0.19.16

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,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.19.16
4
+
5
+ ### Fixed
6
+ - Remote media URLs are now streamed into Zapo instead of being fully materialized as `Uint8Array` in memory before upload. This removes the extra whole-file buffering step that could stall or intermittently fail larger MP3/audio sends.
7
+ - Remote media fetches now have a bounded 120-second transfer timeout and surface provider errors when the source cannot be opened or returns an empty body.
8
+ - Local paths and caller-provided byte arrays keep their existing behavior.
9
+
10
+ ### Performance
11
+ - URL-backed audio/video/image uploads now follow Zapo's recommended streaming media path, keeping memory usage flat while Zapo stages, hashes, encrypts, and uploads the attachment.
12
+
13
+ ## 0.19.15
14
+
15
+ ### Added
16
+ - Added provider-safe payload classification through `message.protocolKinds` and `message.payloadKinds`, also available on quoted messages.
17
+ - Added `payment` to `MessageContentKind` for WhatsApp payment payloads and native-flow payment cards.
18
+ - Added Zapo 1.7.1 payment coverage for `sendPaymentMessage`, `requestPaymentMessage`, `paymentInviteMessage`, `cancelPaymentRequestMessage`, `declinePaymentRequestMessage`, `invoiceMessage`, `paymentReminderMessage`, `splitPaymentMessage`, and `splitPaymentUpdateMessage`.
19
+ - Added group-status wrapper coverage for `groupStatusMessage`, `groupStatusMessageV2`, `groupStatusMentionMessage`, and `groupMentionedMessage` while preserving the nested content classification.
20
+ - Added `catalog_message` classification for Zapo `productMessage` and `orderMessage` payloads.
21
+ - Added native-flow payment detection for the documented `payment_info` and `review_and_pay` flows.
22
+ - Added defensive `malformed_payload` and `native_flow_crash` signals for invalid or structurally unsafe native-flow JSON without exposing raw provider payloads to consumers.
23
+
24
+ ### Compatibility
25
+ - Existing `contentKind` consumers remain compatible; the new protocol/payload arrays are optional.
26
+ - `groupStatus*` and `groupMentionedMessage` are unwrapped as Zapo `FutureProofMessage` containers so text/media inside them continues through the regular normalizer.
27
+
3
28
  ## 0.19.14
4
29
 
5
30
  - Removed the temporary end-to-end AntiEdit diagnostic instrumentation after confirming the group-author identity fix in production.
package/README.md CHANGED
@@ -356,7 +356,26 @@ app.on('message', async (message) => {
356
356
  });
357
357
  ```
358
358
 
359
- Os valores disponíveis são `text`, `image`, `video`, `audio`, `document`, `sticker`, `location`, `contact`, `poll`, `catalog` e `unknown`. Para mídia baixável, continue usando `message.media.kind`.
359
+ Os valores disponíveis são `text`, `image`, `video`, `audio`, `document`, `sticker`, `location`, `contact`, `poll`, `catalog`, `payment` e `unknown`. Para mídia baixável, continue usando `message.media.kind`.
360
+
361
+ Para moderação de payloads especiais, o provider Zapo também expõe duas classificações opcionais:
362
+
363
+ - `message.protocolKinds`: nomes reais do protocolo reconhecidos no `Proto.IMessage`, sem expor o objeto protobuf bruto.
364
+ - `message.payloadKinds`: categorias estáveis do WhaNext: `catalog_message`, `payment_payload`, `group_status_payload`, `payment_info_embedded`, `native_flow_crash` e `malformed_payload`.
365
+
366
+ Um filtro de pagamentos pode cobrir tanto mensagens de pagamento do protocolo quanto os cards PIX/cobrança em native-flow:
367
+
368
+ ```ts
369
+ app.on('message', async (message) => {
370
+ if (message.contentKind === 'payment' && message.isGroup) {
371
+ await app.message.delete(message);
372
+ }
373
+ });
374
+ ```
375
+
376
+ Os wrappers `groupStatusMessage`, `groupStatusMessageV2`, `groupStatusMentionMessage` e `groupMentionedMessage` são desembrulhados antes da classificação normal. Por exemplo, uma imagem dentro de `groupStatusMessageV2` continua com `contentKind === 'image'`, enquanto `payloadKinds` inclui `group_status_payload` e `protocolKinds` preserva o wrapper detectado.
377
+
378
+ `catalog_message` normaliza `productMessage` e `orderMessage`. `payment_info_embedded` cobre os flows `payment_info` e `review_and_pay`. `native_flow_crash` e `malformed_payload` são sinais defensivos do WhaNext, não nomes de campos do protocolo Zapo.
360
379
 
361
380
  ```ts
362
381
  app.on('message', async (message) => {
@@ -518,6 +537,8 @@ const downloaded = await app.media.download(message);
518
537
  await writeFile(`./downloads/${downloaded.fileName ?? message.id}`, downloaded.data);
519
538
  ```
520
539
 
540
+ Ao enviar `{ url }`, o provider abre a origem remota como stream e entrega o fluxo ao pipeline de mídia do Zapo; o arquivo não é carregado inteiro na memória antes do upload. Para arquivos locais, `{ path }` continua sendo a opção mais direta.
541
+
521
542
  `download()` aceita a `Message` recebida, uma `QuotedMessage` ou uma `MessageKey` e devolve o buffer junto dos metadados normalizados. A mídia deve ser baixada enquanto a mensagem ainda está no cache da instância; o provider tenta renovar a URL de mídia automaticamente quando necessário.
522
543
 
523
544
  Para visualização única citada, o provider oficial preserva os metadados do envelope sem expor tipos do Zapo:
package/dist/index.d.ts CHANGED
@@ -72,7 +72,9 @@ type MediaKind = 'image' | 'video' | 'audio' | 'document' | 'sticker';
72
72
  * `contentKind` additionally exposes non-media payloads such as locations,
73
73
  * contacts, polls and catalog/product messages without leaking provider-specific protocol types.
74
74
  */
75
- type MessageContentKind = 'text' | 'image' | 'video' | 'audio' | 'document' | 'sticker' | 'location' | 'contact' | 'poll' | 'catalog' | 'unknown';
75
+ type MessageContentKind = 'text' | 'image' | 'video' | 'audio' | 'document' | 'sticker' | 'location' | 'contact' | 'poll' | 'catalog' | 'payment' | 'unknown';
76
+ type MessageProtocolKind = 'groupStatusMessage' | 'groupStatusMessageV2' | 'groupStatusMentionMessage' | 'groupMentionedMessage' | 'productMessage' | 'orderMessage' | 'sendPaymentMessage' | 'requestPaymentMessage' | 'paymentInviteMessage' | 'cancelPaymentRequestMessage' | 'declinePaymentRequestMessage' | 'invoiceMessage' | 'paymentReminderMessage' | 'splitPaymentMessage' | 'splitPaymentUpdateMessage';
77
+ type MessagePayloadKind = 'catalog_message' | 'payment_payload' | 'group_status_payload' | 'payment_info_embedded' | 'native_flow_crash' | 'malformed_payload';
76
78
  interface MessageMedia {
77
79
  kind: MediaKind;
78
80
  mimetype?: string;
@@ -88,6 +90,8 @@ interface QuotedMessage {
88
90
  hasMedia: boolean;
89
91
  isViewOnce?: boolean;
90
92
  contentKind?: MessageContentKind;
93
+ protocolKinds?: MessageProtocolKind[];
94
+ payloadKinds?: MessagePayloadKind[];
91
95
  media?: MessageMedia;
92
96
  }
93
97
  type InteractiveResponseKind = 'button' | 'list';
@@ -132,6 +136,8 @@ interface Message {
132
136
  isViewOnce: boolean;
133
137
  hasMedia: boolean;
134
138
  contentKind?: MessageContentKind;
139
+ protocolKinds?: MessageProtocolKind[];
140
+ payloadKinds?: MessagePayloadKind[];
135
141
  media?: MessageMedia;
136
142
  quoted?: QuotedMessage;
137
143
  interactive?: InteractiveResponse;
@@ -966,4 +972,4 @@ declare class SqliteMuteStore implements MuteStore {
966
972
  close(): void;
967
973
  }
968
974
 
969
- 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 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 CommandRegistrar, CommandRouter, type CommandRuntimeServices, type CommandScope, type ConcurrencyStrategy, type ConnectionState, type ConnectionUpdate, type CopyCodeButton, type CreateMultiOptions, type CreateOptions, DeferredReply, type DownloadedMedia, type DurationOption, type EnumOption, type ExecutableCommandDefinition, type GroupAccess, type GroupAddressingMode, type GroupParticipant, type GroupParticipantAction, type GroupParticipantsChanged, type GroupRole, type GroupSnapshot, type GuardResult, 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 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 QuickReplyButton, type QuotedMessage, type ReconnectOptions, type RegisteredCommand, type RemoveMuteResult, type ReplyOptions, type RepostMessageOptions, type RouterOptions, type SentMessage, SqliteMuteStore, 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 };
975
+ 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 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 CommandRegistrar, CommandRouter, type CommandRuntimeServices, type CommandScope, type ConcurrencyStrategy, type ConnectionState, type ConnectionUpdate, type CopyCodeButton, type CreateMultiOptions, type CreateOptions, DeferredReply, type DownloadedMedia, type DurationOption, type EnumOption, type ExecutableCommandDefinition, type GroupAccess, type GroupAddressingMode, type GroupParticipant, type GroupParticipantAction, type GroupParticipantsChanged, type GroupRole, type GroupSnapshot, type GuardResult, 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 QuickReplyButton, type QuotedMessage, type ReconnectOptions, type RegisteredCommand, type RemoveMuteResult, type ReplyOptions, type RepostMessageOptions, type RouterOptions, type SentMessage, SqliteMuteStore, 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
@@ -2406,6 +2406,7 @@ var Browser = /* @__PURE__ */ ((Browser2) => {
2406
2406
  // src/provider/zapo/zapo-provider.ts
2407
2407
  import { mkdir, stat } from "fs/promises";
2408
2408
  import { createRequire as createRequire2 } from "module";
2409
+ import { Readable } from "stream";
2409
2410
  import { basename, dirname as dirname2, join, resolve as resolve2 } from "path";
2410
2411
  import { createMediaProcessor } from "@zapo-js/media-utils";
2411
2412
  import { createSqliteStore } from "@zapo-js/store-sqlite";
@@ -2416,9 +2417,37 @@ import {
2416
2417
  } from "zapo-js";
2417
2418
 
2418
2419
  // src/provider/zapo/normalize-message.ts
2420
+ var GROUP_STATUS_PROTOCOL_KINDS = [
2421
+ "groupStatusMessage",
2422
+ "groupStatusMessageV2",
2423
+ "groupStatusMentionMessage",
2424
+ "groupMentionedMessage"
2425
+ ];
2426
+ var PAYMENT_PROTOCOL_KINDS = [
2427
+ "sendPaymentMessage",
2428
+ "requestPaymentMessage",
2429
+ "paymentInviteMessage",
2430
+ "cancelPaymentRequestMessage",
2431
+ "declinePaymentRequestMessage",
2432
+ "invoiceMessage",
2433
+ "paymentReminderMessage",
2434
+ "splitPaymentMessage",
2435
+ "splitPaymentUpdateMessage"
2436
+ ];
2437
+ var CATALOG_PROTOCOL_KINDS = [
2438
+ "productMessage",
2439
+ "orderMessage"
2440
+ ];
2441
+ var NATIVE_FLOW_PAYMENT_NAMES = /* @__PURE__ */ new Set([
2442
+ "payment_info",
2443
+ "review_and_pay"
2444
+ ]);
2445
+ var MAX_NATIVE_FLOW_JSON_BYTES = 128 * 1024;
2446
+ var MAX_NATIVE_FLOW_DEPTH = 32;
2447
+ var MAX_NATIVE_FLOW_NODES = 4096;
2419
2448
  function unwrapZapoMessageContent(input) {
2420
2449
  if (!input) return void 0;
2421
- const nested = input.ephemeralMessage?.message ?? input.viewOnceMessage?.message ?? input.viewOnceMessageV2?.message ?? viewOnceV2ExtensionMessage(input) ?? input.deviceSentMessage?.message ?? input.documentWithCaptionMessage?.message ?? editedWrapperMessage(input);
2450
+ const nested = input.ephemeralMessage?.message ?? input.viewOnceMessage?.message ?? input.viewOnceMessageV2?.message ?? viewOnceV2ExtensionMessage(input) ?? input.deviceSentMessage?.message ?? input.documentWithCaptionMessage?.message ?? editedWrapperMessage(input) ?? futureProofMessage(input, "groupStatusMessage") ?? futureProofMessage(input, "groupStatusMessageV2") ?? futureProofMessage(input, "groupStatusMentionMessage") ?? futureProofMessage(input, "groupMentionedMessage");
2422
2451
  return nested ? unwrapZapoMessageContent(nested) : input;
2423
2452
  }
2424
2453
  function editedWrapperMessage(input) {
@@ -2429,12 +2458,16 @@ function viewOnceV2ExtensionMessage(input) {
2429
2458
  const extension = input.viewOnceMessageV2Extension;
2430
2459
  return extension?.message ?? void 0;
2431
2460
  }
2461
+ function futureProofMessage(input, key) {
2462
+ const wrapper = input[key];
2463
+ return wrapper?.message ?? void 0;
2464
+ }
2432
2465
  function isZapoViewOnceContent(input) {
2433
2466
  if (!input) return false;
2434
2467
  if (input.viewOnceMessage?.message || input.viewOnceMessageV2?.message || viewOnceV2ExtensionMessage(input)) {
2435
2468
  return true;
2436
2469
  }
2437
- const nested = input.ephemeralMessage?.message ?? input.deviceSentMessage?.message ?? input.documentWithCaptionMessage?.message ?? editedWrapperMessage(input);
2470
+ const nested = input.ephemeralMessage?.message ?? input.deviceSentMessage?.message ?? input.documentWithCaptionMessage?.message ?? editedWrapperMessage(input) ?? futureProofMessage(input, "groupStatusMessage") ?? futureProofMessage(input, "groupStatusMessageV2") ?? futureProofMessage(input, "groupStatusMentionMessage") ?? futureProofMessage(input, "groupMentionedMessage");
2438
2471
  return nested ? isZapoViewOnceContent(nested) : false;
2439
2472
  }
2440
2473
  function extractQuotedZapoMessage(input) {
@@ -2462,6 +2495,7 @@ function normalizeZapoMessage(input) {
2462
2495
  if (!chatId || !id || !event.message) {
2463
2496
  return void 0;
2464
2497
  }
2498
+ const payloadInspection = inspectZapoPayload(event.message);
2465
2499
  const content = unwrapZapoMessageContent(event.message);
2466
2500
  if (!content) {
2467
2501
  return void 0;
@@ -2488,7 +2522,7 @@ function normalizeZapoMessage(input) {
2488
2522
  const mentionedUsers = mentionedIds.map((identity) => User.fromIdentities([identity]));
2489
2523
  const viewOnce = isZapoViewOnceContent(event.message);
2490
2524
  const media = getMedia(type, node, viewOnce);
2491
- const contentKind = getContentKind(type);
2525
+ const contentKind = payloadInspection.payloadKinds.includes("payment_payload") || payloadInspection.payloadKinds.includes("payment_info_embedded") ? "payment" : getContentKind(type);
2492
2526
  const text = getText(content);
2493
2527
  const caption = getCaption(content);
2494
2528
  const quoted = getQuoted(context, chatId);
@@ -2510,6 +2544,12 @@ function normalizeZapoMessage(input) {
2510
2544
  hasMedia: media !== void 0,
2511
2545
  contentKind
2512
2546
  };
2547
+ if (payloadInspection.protocolKinds.length > 0) {
2548
+ message.protocolKinds = payloadInspection.protocolKinds;
2549
+ }
2550
+ if (payloadInspection.payloadKinds.length > 0) {
2551
+ message.payloadKinds = payloadInspection.payloadKinds;
2552
+ }
2513
2553
  if (senderJid !== void 0) message.senderJid = senderJid;
2514
2554
  if (senderLid !== void 0) {
2515
2555
  message.lid = senderLid;
@@ -2534,6 +2574,131 @@ function normalizeZapoKey(key) {
2534
2574
  }
2535
2575
  return normalized;
2536
2576
  }
2577
+ function inspectZapoPayload(input) {
2578
+ const protocolKinds = /* @__PURE__ */ new Set();
2579
+ const payloadKinds = /* @__PURE__ */ new Set();
2580
+ inspectMessageLayer(input, protocolKinds, payloadKinds, 0);
2581
+ return {
2582
+ protocolKinds: [...protocolKinds],
2583
+ payloadKinds: [...payloadKinds]
2584
+ };
2585
+ }
2586
+ function inspectMessageLayer(input, protocolKinds, payloadKinds, depth) {
2587
+ if (!input) return;
2588
+ if (depth > 16) {
2589
+ payloadKinds.add("malformed_payload");
2590
+ return;
2591
+ }
2592
+ for (const kind of GROUP_STATUS_PROTOCOL_KINDS) {
2593
+ if (input[kind] !== null && input[kind] !== void 0) {
2594
+ protocolKinds.add(kind);
2595
+ payloadKinds.add("group_status_payload");
2596
+ if (!futureProofMessage(input, kind)) payloadKinds.add("malformed_payload");
2597
+ }
2598
+ }
2599
+ for (const kind of PAYMENT_PROTOCOL_KINDS) {
2600
+ if (input[kind] !== null && input[kind] !== void 0) {
2601
+ protocolKinds.add(kind);
2602
+ payloadKinds.add("payment_payload");
2603
+ }
2604
+ }
2605
+ for (const kind of CATALOG_PROTOCOL_KINDS) {
2606
+ if (input[kind] !== null && input[kind] !== void 0) {
2607
+ protocolKinds.add(kind);
2608
+ payloadKinds.add("catalog_message");
2609
+ }
2610
+ }
2611
+ inspectNativeFlow(input, payloadKinds);
2612
+ for (const nested of wrappedMessages(input)) {
2613
+ inspectMessageLayer(nested, protocolKinds, payloadKinds, depth + 1);
2614
+ }
2615
+ }
2616
+ function wrappedMessages(input) {
2617
+ const messages = [
2618
+ input.ephemeralMessage?.message,
2619
+ input.viewOnceMessage?.message,
2620
+ input.viewOnceMessageV2?.message,
2621
+ viewOnceV2ExtensionMessage(input),
2622
+ input.deviceSentMessage?.message,
2623
+ input.documentWithCaptionMessage?.message,
2624
+ editedWrapperMessage(input),
2625
+ futureProofMessage(input, "groupStatusMessage"),
2626
+ futureProofMessage(input, "groupStatusMessageV2"),
2627
+ futureProofMessage(input, "groupStatusMentionMessage"),
2628
+ futureProofMessage(input, "groupMentionedMessage")
2629
+ ];
2630
+ return messages.filter((message) => message !== null && message !== void 0);
2631
+ }
2632
+ function inspectNativeFlow(input, payloadKinds) {
2633
+ const interactive = input.interactiveMessage;
2634
+ const nativeFlow = interactive?.nativeFlowMessage;
2635
+ if (nativeFlow) {
2636
+ inspectNativeFlowJson(nativeFlow.messageParamsJson, payloadKinds);
2637
+ for (const button of nativeFlow.buttons ?? []) {
2638
+ if (button.name && NATIVE_FLOW_PAYMENT_NAMES.has(button.name)) {
2639
+ payloadKinds.add("payment_info_embedded");
2640
+ }
2641
+ inspectNativeFlowJson(button.buttonParamsJson, payloadKinds);
2642
+ }
2643
+ }
2644
+ const response = input.interactiveResponseMessage;
2645
+ const nativeResponse = response?.nativeFlowResponseMessage;
2646
+ if (nativeResponse?.name && NATIVE_FLOW_PAYMENT_NAMES.has(nativeResponse.name)) {
2647
+ payloadKinds.add("payment_info_embedded");
2648
+ }
2649
+ inspectNativeFlowJson(nativeResponse?.paramsJson, payloadKinds);
2650
+ const buttonsMessage = input.buttonsMessage;
2651
+ for (const button of buttonsMessage?.buttons ?? []) {
2652
+ const flow = button.nativeFlowInfo;
2653
+ if (flow?.name && NATIVE_FLOW_PAYMENT_NAMES.has(flow.name)) {
2654
+ payloadKinds.add("payment_info_embedded");
2655
+ }
2656
+ inspectNativeFlowJson(flow?.paramsJson, payloadKinds);
2657
+ }
2658
+ }
2659
+ function inspectNativeFlowJson(json, payloadKinds) {
2660
+ if (!json) return;
2661
+ if (Buffer.byteLength(json, "utf8") > MAX_NATIVE_FLOW_JSON_BYTES) {
2662
+ payloadKinds.add("native_flow_crash");
2663
+ payloadKinds.add("malformed_payload");
2664
+ return;
2665
+ }
2666
+ let parsed;
2667
+ try {
2668
+ parsed = JSON.parse(json);
2669
+ } catch {
2670
+ payloadKinds.add("malformed_payload");
2671
+ return;
2672
+ }
2673
+ if (!isSafeNativeFlowJson(parsed)) {
2674
+ payloadKinds.add("native_flow_crash");
2675
+ payloadKinds.add("malformed_payload");
2676
+ }
2677
+ }
2678
+ function isSafeNativeFlowJson(root) {
2679
+ const queue = [{ value: root, depth: 0 }];
2680
+ let nodes = 0;
2681
+ while (queue.length > 0) {
2682
+ const current = queue.shift();
2683
+ if (!current) break;
2684
+ nodes += 1;
2685
+ if (nodes > MAX_NATIVE_FLOW_NODES || current.depth > MAX_NATIVE_FLOW_DEPTH) {
2686
+ return false;
2687
+ }
2688
+ if (Array.isArray(current.value)) {
2689
+ for (const value of current.value) {
2690
+ queue.push({ value, depth: current.depth + 1 });
2691
+ }
2692
+ continue;
2693
+ }
2694
+ if (typeof current.value === "object" && current.value !== null) {
2695
+ for (const value of Object.values(current.value)) {
2696
+ queue.push({ value, depth: current.depth + 1 });
2697
+ }
2698
+ }
2699
+ }
2700
+ return true;
2701
+ }
2537
2702
  function contentNode(content) {
2538
2703
  const order = [
2539
2704
  "conversation",
@@ -2556,6 +2721,7 @@ function contentNode(content) {
2556
2721
  "pollCreationMessageV5",
2557
2722
  "productMessage",
2558
2723
  "orderMessage",
2724
+ "interactiveMessage",
2559
2725
  "interactiveResponseMessage"
2560
2726
  ];
2561
2727
  const type = order.find((key) => content[key] !== null && content[key] !== void 0);
@@ -2568,6 +2734,7 @@ function getContentKind(type) {
2568
2734
  case "buttonsResponseMessage":
2569
2735
  case "listResponseMessage":
2570
2736
  case "templateButtonReplyMessage":
2737
+ case "interactiveMessage":
2571
2738
  case "interactiveResponseMessage":
2572
2739
  return "text";
2573
2740
  case "imageMessage":
@@ -2599,7 +2766,11 @@ function getContentKind(type) {
2599
2766
  }
2600
2767
  }
2601
2768
  function getText(content) {
2602
- return content.conversation ?? content.extendedTextMessage?.text ?? content.buttonsResponseMessage?.selectedDisplayText ?? content.listResponseMessage?.title ?? content.templateButtonReplyMessage?.selectedDisplayText ?? content.pollCreationMessage?.name ?? content.pollCreationMessageV2?.name ?? content.pollCreationMessageV3?.name ?? content.pollCreationMessageV5?.name ?? getNativeFlowDisplayText(content) ?? void 0;
2769
+ return content.conversation ?? content.extendedTextMessage?.text ?? content.buttonsResponseMessage?.selectedDisplayText ?? content.listResponseMessage?.title ?? content.templateButtonReplyMessage?.selectedDisplayText ?? content.pollCreationMessage?.name ?? content.pollCreationMessageV2?.name ?? content.pollCreationMessageV3?.name ?? content.pollCreationMessageV5?.name ?? interactiveMessageBodyText(content) ?? getNativeFlowDisplayText(content) ?? void 0;
2770
+ }
2771
+ function interactiveMessageBodyText(content) {
2772
+ const interactive = content.interactiveMessage;
2773
+ return interactive?.body?.text ?? void 0;
2603
2774
  }
2604
2775
  function getInteractiveResponse(content) {
2605
2776
  const buttons = content.buttonsResponseMessage;
@@ -2700,6 +2871,7 @@ function getMedia(type, node, wrapperViewOnce) {
2700
2871
  }
2701
2872
  function getQuoted(context, chatId) {
2702
2873
  if (!context?.stanzaId || !context.quotedMessage) return void 0;
2874
+ const payloadInspection = inspectZapoPayload(context.quotedMessage);
2703
2875
  const content = unwrapZapoMessageContent(context.quotedMessage);
2704
2876
  if (!content) return void 0;
2705
2877
  const { type, node } = contentNode(content);
@@ -2714,8 +2886,14 @@ function getQuoted(context, chatId) {
2714
2886
  },
2715
2887
  hasMedia: media !== void 0,
2716
2888
  isViewOnce: media?.viewOnce ?? false,
2717
- contentKind: getContentKind(type)
2889
+ contentKind: payloadInspection.payloadKinds.includes("payment_payload") || payloadInspection.payloadKinds.includes("payment_info_embedded") ? "payment" : getContentKind(type)
2718
2890
  };
2891
+ if (payloadInspection.protocolKinds.length > 0) {
2892
+ quoted.protocolKinds = payloadInspection.protocolKinds;
2893
+ }
2894
+ if (payloadInspection.payloadKinds.length > 0) {
2895
+ quoted.payloadKinds = payloadInspection.payloadKinds;
2896
+ }
2719
2897
  const text = getText(content) ?? getCaption(content);
2720
2898
  if (text !== void 0) quoted.text = text;
2721
2899
  if (senderId !== void 0) {
@@ -2753,6 +2931,7 @@ var fatalDisconnectReasons = /* @__PURE__ */ new Set([
2753
2931
  ]);
2754
2932
  var fatalDisconnectCodes = /* @__PURE__ */ new Set([401, 403, 405, 406, 409, 516]);
2755
2933
  var sharedMediaProcessor = createMediaProcessor();
2934
+ var REMOTE_MEDIA_TIMEOUT_MS = 12e4;
2756
2935
  var messageSnapshotRetentionSeconds = 7 * 24 * 60 * 60;
2757
2936
  var messageSnapshotMaxPerSession = 2e4;
2758
2937
  var messageSnapshotPruneInterval = 256;
@@ -3791,8 +3970,20 @@ var ZapoProvider = class {
3791
3970
  async #media(source) {
3792
3971
  if (source instanceof Uint8Array) return source;
3793
3972
  if ("path" in source) return source.path;
3794
- const response = await fetch(source.url);
3973
+ let response;
3974
+ try {
3975
+ response = await fetch(source.url, {
3976
+ signal: AbortSignal.timeout(REMOTE_MEDIA_TIMEOUT_MS)
3977
+ });
3978
+ } catch (error) {
3979
+ throw new WhaNextError(
3980
+ "PROVIDER_ERROR",
3981
+ "Could not open the remote media source.",
3982
+ { cause: error, recoverable: true }
3983
+ );
3984
+ }
3795
3985
  if (!response.ok) {
3986
+ await response.body?.cancel().catch(() => void 0);
3796
3987
  throw new WhaNextError(
3797
3988
  "PROVIDER_ERROR",
3798
3989
  "Could not download the remote media source.",
@@ -3802,7 +3993,16 @@ var ZapoProvider = class {
3802
3993
  }
3803
3994
  );
3804
3995
  }
3805
- return new Uint8Array(await response.arrayBuffer());
3996
+ if (!response.body) {
3997
+ throw new WhaNextError(
3998
+ "PROVIDER_ERROR",
3999
+ "The remote media source returned an empty response body.",
4000
+ { recoverable: true }
4001
+ );
4002
+ }
4003
+ return Readable.fromWeb(
4004
+ response.body
4005
+ );
3806
4006
  }
3807
4007
  #mentions(mentions) {
3808
4008
  return mentions.map((mention) => typeof mention === "string" ? mention : mention.mentionId);