@adatechnology/meta-whatsapp-module 0.2.0-rc.3 → 0.2.0-rc.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,8 +1,10 @@
1
1
  import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
2
2
  import { PgDatabase, PgQueryResultHKT } from 'drizzle-orm/pg-core';
3
3
  import * as _adatechnology_meta_whatsapp_contracts from '@adatechnology/meta-whatsapp-contracts';
4
- import { SessionState, SessionMode, ConversationSummary, FlowGraphData, FlowGraphSummary, LiveFlowPosition, WhatsAppSettings, MessageDirection, MessageSender, MessageStatus, RealtimeNotifierInterface, ChannelAdapterInterface, ObjectStorageInterface, FlowActionKind, FlowActionHandler, ConversationSession, MetaWhatsAppHooks, SubjectResolverInterface, CatalogPort } from '@adatechnology/meta-whatsapp-contracts';
4
+ import { TranscriptionMode, SessionState, SessionMode, ConversationSummary, MessageDirection, MessageSender, MessageStatus, CacheInterface, FlowGraphData, FlowGraphSummary, LiveFlowPosition, WhatsAppSettings, RealtimeNotifierInterface, ChannelAdapterInterface, ObjectStorageInterface, FlowActionKind, FlowActionHandler, ConversationSession, WhatsAppMessage, WhatsAppStatus, MetaWhatsAppHooks, SubjectResolverInterface, CatalogPort } from '@adatechnology/meta-whatsapp-contracts';
5
+ export { PREVIEW_MEDIA_ID_PREFIX, TranscriptionMode, resolvePreviewUploadId, toPreviewMediaId } from '@adatechnology/meta-whatsapp-contracts';
5
6
  import { WhatsAppMessageProvider } from '@adatechnology/meta-whatsapp-provider';
7
+ export { WEBHOOK_CLAIM_TTL_SECONDS, WEBHOOK_NONCE_TTL_SECONDS } from '@adatechnology/meta-graph-core';
6
8
 
7
9
  type MetaWhatsAppDatabase = PgDatabase<PgQueryResultHKT, any, any>;
8
10
  type DrizzleMigrateFunction = (db: never, config: {
@@ -10,6 +12,72 @@ type DrizzleMigrateFunction = (db: never, config: {
10
12
  migrationsTable?: string;
11
13
  }) => Promise<void>;
12
14
 
15
+ /**
16
+ * Vocabulário de transcrição de áudio do módulo.
17
+ *
18
+ * Fica em arquivo próprio porque o schema e os dois use-cases (ingestão automática e sob demanda)
19
+ * precisam dos mesmos tipos, e pendurá-los em qualquer um dos três faria os outros dois importarem
20
+ * de dentro de uma camada que não é a deles.
21
+ */
22
+
23
+ declare const TRANSCRIPTION_STATUS: {
24
+ /** Falhou de forma retriável (cota, rede, 5xx) — vai sair quando alguém tentar de novo. */
25
+ readonly PENDING: "pending";
26
+ /** Processado. Texto vazio aqui é áudio em silêncio, e NÃO deve ser reprocessado. */
27
+ readonly DONE: "done";
28
+ /** Falha definitiva do engine (credencial, áudio corrompido, arquivo grande demais). */
29
+ readonly FAILED: "failed";
30
+ /** Nenhum engine da cadeia aceita o formato. Retentar não conserta codec. */
31
+ readonly UNSUPPORTED: "unsupported";
32
+ };
33
+ type TranscriptionStatus = (typeof TRANSCRIPTION_STATUS)[keyof typeof TRANSCRIPTION_STATUS];
34
+ /**
35
+ * Quando transcrever.
36
+ *
37
+ * `auto` transcreve durante a ingestão da mídia, onde o buffer do áudio JÁ está em memória — não
38
+ * custa um segundo download do storage. `onDemand` só transcreve quando o atendente pede, o que
39
+ * troca latência na interface por não gastar cota com áudio que ninguém vai ler.
40
+ *
41
+ * O tipo vem do contrato (o painel escolhe, a API transporta, o módulo obedece) e `satisfies`
42
+ * garante em tempo de compilação que estes valores continuam sendo exatamente os de lá — sem isso,
43
+ * um modo novo no contrato passaria despercebido aqui.
44
+ */
45
+ declare const TRANSCRIPTION_MODE: {
46
+ readonly AUTO: "auto";
47
+ readonly ON_DEMAND: "onDemand";
48
+ };
49
+
50
+ /**
51
+ * O contrato mínimo de `@adatechnology/audio-transcription-provider`, declarado aqui em vez de
52
+ * importado — mesma decisão do `MessageModerator` em `LogMessage.use-case.ts`.
53
+ *
54
+ * Assim o módulo não ganha dependência de pacote por um recurso opcional, e transcrição não fica
55
+ * amarrada a WhatsApp: o que atravessa esta fronteira é um buffer de áudio e um mime.
56
+ */
57
+ type AudioTranscriber = {
58
+ readonly name: string;
59
+ transcribe: (input: {
60
+ buffer: Buffer;
61
+ mimeType: string;
62
+ languageHint?: string;
63
+ }) => Promise<{
64
+ text: string;
65
+ language?: string;
66
+ durationSeconds?: number;
67
+ engine: string;
68
+ }>;
69
+ };
70
+ /**
71
+ * Recorte do erro do provider que o módulo precisa ler para escolher entre `'pending'` e
72
+ * `'failed'`. Estrutural, não `instanceof`: o provider é opcional e pode nem estar instalado, e um
73
+ * `instanceof` contra classe ausente não compila.
74
+ */
75
+ declare function isRetriableTranscriptionError(error: unknown): boolean;
76
+ declare function isUnsupportedTranscriptionError(error: unknown): boolean;
77
+ declare function transcriptionRetryAfterSeconds(error: unknown): number | undefined;
78
+ /** Mime de áudio? Só áudio é transcrito — vídeo, imagem e documento passam sem tocar no engine. */
79
+ declare function isAudioMimeType(mimeType: string | undefined | null): boolean;
80
+
13
81
  declare const metaWhatsAppSchema: drizzle_orm_pg_core.PgSchema<"meta_whatsapp">;
14
82
  declare const sessions: drizzle_orm_pg_core.PgTableWithColumns<{
15
83
  name: "sessions";
@@ -431,7 +499,7 @@ declare const messages: drizzle_orm_pg_core.PgTableWithColumns<{
431
499
  identity: undefined;
432
500
  generated: undefined;
433
501
  }, {}, {
434
- length: 16;
502
+ length: 32;
435
503
  }>;
436
504
  content: drizzle_orm_pg_core.PgColumn<{
437
505
  name: "content";
@@ -524,6 +592,117 @@ declare const messages: drizzle_orm_pg_core.PgTableWithColumns<{
524
592
  identity: undefined;
525
593
  generated: undefined;
526
594
  }, {}, {}>;
595
+ moderationFlagged: drizzle_orm_pg_core.PgColumn<{
596
+ name: "moderation_flagged";
597
+ tableName: "messages";
598
+ dataType: "boolean";
599
+ columnType: "PgBoolean";
600
+ data: boolean;
601
+ driverParam: boolean;
602
+ notNull: false;
603
+ hasDefault: false;
604
+ isPrimaryKey: false;
605
+ isAutoincrement: false;
606
+ hasRuntimeDefault: false;
607
+ enumValues: undefined;
608
+ baseColumn: never;
609
+ identity: undefined;
610
+ generated: undefined;
611
+ }, {}, {}>;
612
+ moderationTerms: drizzle_orm_pg_core.PgColumn<{
613
+ name: "moderation_terms";
614
+ tableName: "messages";
615
+ dataType: "json";
616
+ columnType: "PgJsonb";
617
+ data: string[];
618
+ driverParam: unknown;
619
+ notNull: false;
620
+ hasDefault: false;
621
+ isPrimaryKey: false;
622
+ isAutoincrement: false;
623
+ hasRuntimeDefault: false;
624
+ enumValues: undefined;
625
+ baseColumn: never;
626
+ identity: undefined;
627
+ generated: undefined;
628
+ }, {}, {
629
+ $type: string[];
630
+ }>;
631
+ transcriptionStatus: drizzle_orm_pg_core.PgColumn<{
632
+ name: "transcription_status";
633
+ tableName: "messages";
634
+ dataType: "string";
635
+ columnType: "PgVarchar";
636
+ data: TranscriptionStatus;
637
+ driverParam: string;
638
+ notNull: false;
639
+ hasDefault: false;
640
+ isPrimaryKey: false;
641
+ isAutoincrement: false;
642
+ hasRuntimeDefault: false;
643
+ enumValues: [string, ...string[]];
644
+ baseColumn: never;
645
+ identity: undefined;
646
+ generated: undefined;
647
+ }, {}, {
648
+ length: 16;
649
+ $type: TranscriptionStatus;
650
+ }>;
651
+ transcriptionText: drizzle_orm_pg_core.PgColumn<{
652
+ name: "transcription_text";
653
+ tableName: "messages";
654
+ dataType: "string";
655
+ columnType: "PgText";
656
+ data: string;
657
+ driverParam: string;
658
+ notNull: false;
659
+ hasDefault: false;
660
+ isPrimaryKey: false;
661
+ isAutoincrement: false;
662
+ hasRuntimeDefault: false;
663
+ enumValues: [string, ...string[]];
664
+ baseColumn: never;
665
+ identity: undefined;
666
+ generated: undefined;
667
+ }, {}, {}>;
668
+ transcriptionLanguage: drizzle_orm_pg_core.PgColumn<{
669
+ name: "transcription_language";
670
+ tableName: "messages";
671
+ dataType: "string";
672
+ columnType: "PgVarchar";
673
+ data: string;
674
+ driverParam: string;
675
+ notNull: false;
676
+ hasDefault: false;
677
+ isPrimaryKey: false;
678
+ isAutoincrement: false;
679
+ hasRuntimeDefault: false;
680
+ enumValues: [string, ...string[]];
681
+ baseColumn: never;
682
+ identity: undefined;
683
+ generated: undefined;
684
+ }, {}, {
685
+ length: 32;
686
+ }>;
687
+ transcriptionEngine: drizzle_orm_pg_core.PgColumn<{
688
+ name: "transcription_engine";
689
+ tableName: "messages";
690
+ dataType: "string";
691
+ columnType: "PgVarchar";
692
+ data: string;
693
+ driverParam: string;
694
+ notNull: false;
695
+ hasDefault: false;
696
+ isPrimaryKey: false;
697
+ isAutoincrement: false;
698
+ hasRuntimeDefault: false;
699
+ enumValues: [string, ...string[]];
700
+ baseColumn: never;
701
+ identity: undefined;
702
+ generated: undefined;
703
+ }, {}, {
704
+ length: 32;
705
+ }>;
527
706
  createdAt: drizzle_orm_pg_core.PgColumn<{
528
707
  name: "created_at";
529
708
  tableName: "messages";
@@ -544,13 +723,25 @@ declare const messages: drizzle_orm_pg_core.PgTableWithColumns<{
544
723
  };
545
724
  dialect: "pg";
546
725
  }>;
547
- declare const flowGraphs: drizzle_orm_pg_core.PgTableWithColumns<{
548
- name: "flow_graphs";
726
+ /**
727
+ * Biblioteca de arquivos da conversa.
728
+ *
729
+ * Tabela própria, e não derivada de `messages.payload`: o painel precisa de `source` e `linkedAt`,
730
+ * que não existem no payload; busca por nome de arquivo quer índice, não varredura de jsonb; e
731
+ * `messageId` anulável é o que permite o atendente anexar documento à conversa sem que exista uma
732
+ * mensagem correspondente.
733
+ *
734
+ * `sessionId` em cascata apaga a LINHA junto com a conversa, mas não o binário no storage — quem
735
+ * apaga objeto é passo de aplicação (listar `uploadId` → apagar no storage → apagar a sessão).
736
+ * Confiar só na FK deixaria objeto órfão sendo cobrado para sempre.
737
+ */
738
+ declare const documents: drizzle_orm_pg_core.PgTableWithColumns<{
739
+ name: "documents";
549
740
  schema: "meta_whatsapp";
550
741
  columns: {
551
742
  id: drizzle_orm_pg_core.PgColumn<{
552
743
  name: "id";
553
- tableName: "flow_graphs";
744
+ tableName: "documents";
554
745
  dataType: "string";
555
746
  columnType: "PgUUID";
556
747
  data: string;
@@ -567,7 +758,7 @@ declare const flowGraphs: drizzle_orm_pg_core.PgTableWithColumns<{
567
758
  }, {}, {}>;
568
759
  companyId: drizzle_orm_pg_core.PgColumn<{
569
760
  name: "company_id";
570
- tableName: "flow_graphs";
761
+ tableName: "documents";
571
762
  dataType: "string";
572
763
  columnType: "PgUUID";
573
764
  data: string;
@@ -582,11 +773,11 @@ declare const flowGraphs: drizzle_orm_pg_core.PgTableWithColumns<{
582
773
  identity: undefined;
583
774
  generated: undefined;
584
775
  }, {}, {}>;
585
- key: drizzle_orm_pg_core.PgColumn<{
586
- name: "key";
587
- tableName: "flow_graphs";
776
+ sessionId: drizzle_orm_pg_core.PgColumn<{
777
+ name: "session_id";
778
+ tableName: "documents";
588
779
  dataType: "string";
589
- columnType: "PgVarchar";
780
+ columnType: "PgUUID";
590
781
  data: string;
591
782
  driverParam: string;
592
783
  notNull: true;
@@ -594,16 +785,31 @@ declare const flowGraphs: drizzle_orm_pg_core.PgTableWithColumns<{
594
785
  isPrimaryKey: false;
595
786
  isAutoincrement: false;
596
787
  hasRuntimeDefault: false;
597
- enumValues: [string, ...string[]];
788
+ enumValues: undefined;
598
789
  baseColumn: never;
599
790
  identity: undefined;
600
791
  generated: undefined;
601
- }, {}, {
602
- length: 64;
603
- }>;
604
- label: drizzle_orm_pg_core.PgColumn<{
605
- name: "label";
606
- tableName: "flow_graphs";
792
+ }, {}, {}>;
793
+ messageId: drizzle_orm_pg_core.PgColumn<{
794
+ name: "message_id";
795
+ tableName: "documents";
796
+ dataType: "string";
797
+ columnType: "PgUUID";
798
+ data: string;
799
+ driverParam: string;
800
+ notNull: false;
801
+ hasDefault: false;
802
+ isPrimaryKey: false;
803
+ isAutoincrement: false;
804
+ hasRuntimeDefault: false;
805
+ enumValues: undefined;
806
+ baseColumn: never;
807
+ identity: undefined;
808
+ generated: undefined;
809
+ }, {}, {}>;
810
+ uploadId: drizzle_orm_pg_core.PgColumn<{
811
+ name: "upload_id";
812
+ tableName: "documents";
607
813
  dataType: "string";
608
814
  columnType: "PgVarchar";
609
815
  data: string;
@@ -618,11 +824,11 @@ declare const flowGraphs: drizzle_orm_pg_core.PgTableWithColumns<{
618
824
  identity: undefined;
619
825
  generated: undefined;
620
826
  }, {}, {
621
- length: 120;
827
+ length: 256;
622
828
  }>;
623
- startNodeId: drizzle_orm_pg_core.PgColumn<{
624
- name: "start_node_id";
625
- tableName: "flow_graphs";
829
+ filename: drizzle_orm_pg_core.PgColumn<{
830
+ name: "filename";
831
+ tableName: "documents";
626
832
  dataType: "string";
627
833
  columnType: "PgVarchar";
628
834
  data: string;
@@ -637,36 +843,36 @@ declare const flowGraphs: drizzle_orm_pg_core.PgTableWithColumns<{
637
843
  identity: undefined;
638
844
  generated: undefined;
639
845
  }, {}, {
640
- length: 64;
846
+ length: 512;
641
847
  }>;
642
- nodes: drizzle_orm_pg_core.PgColumn<{
643
- name: "nodes";
644
- tableName: "flow_graphs";
645
- dataType: "json";
646
- columnType: "PgJsonb";
647
- data: Record<string, unknown>;
648
- driverParam: unknown;
848
+ mimeType: drizzle_orm_pg_core.PgColumn<{
849
+ name: "mime_type";
850
+ tableName: "documents";
851
+ dataType: "string";
852
+ columnType: "PgVarchar";
853
+ data: string;
854
+ driverParam: string;
649
855
  notNull: true;
650
- hasDefault: true;
856
+ hasDefault: false;
651
857
  isPrimaryKey: false;
652
858
  isAutoincrement: false;
653
859
  hasRuntimeDefault: false;
654
- enumValues: undefined;
860
+ enumValues: [string, ...string[]];
655
861
  baseColumn: never;
656
862
  identity: undefined;
657
863
  generated: undefined;
658
864
  }, {}, {
659
- $type: Record<string, unknown>;
865
+ length: 128;
660
866
  }>;
661
- version: drizzle_orm_pg_core.PgColumn<{
662
- name: "version";
663
- tableName: "flow_graphs";
867
+ sizeBytes: drizzle_orm_pg_core.PgColumn<{
868
+ name: "size_bytes";
869
+ tableName: "documents";
664
870
  dataType: "number";
665
871
  columnType: "PgInteger";
666
872
  data: number;
667
873
  driverParam: string | number;
668
874
  notNull: true;
669
- hasDefault: true;
875
+ hasDefault: false;
670
876
  isPrimaryKey: false;
671
877
  isAutoincrement: false;
672
878
  hasRuntimeDefault: false;
@@ -675,31 +881,33 @@ declare const flowGraphs: drizzle_orm_pg_core.PgTableWithColumns<{
675
881
  identity: undefined;
676
882
  generated: undefined;
677
883
  }, {}, {}>;
678
- showInMenu: drizzle_orm_pg_core.PgColumn<{
679
- name: "show_in_menu";
680
- tableName: "flow_graphs";
681
- dataType: "boolean";
682
- columnType: "PgBoolean";
683
- data: boolean;
684
- driverParam: boolean;
685
- notNull: true;
686
- hasDefault: true;
884
+ sha256: drizzle_orm_pg_core.PgColumn<{
885
+ name: "sha256";
886
+ tableName: "documents";
887
+ dataType: "string";
888
+ columnType: "PgVarchar";
889
+ data: string;
890
+ driverParam: string;
891
+ notNull: false;
892
+ hasDefault: false;
687
893
  isPrimaryKey: false;
688
894
  isAutoincrement: false;
689
895
  hasRuntimeDefault: false;
690
- enumValues: undefined;
896
+ enumValues: [string, ...string[]];
691
897
  baseColumn: never;
692
898
  identity: undefined;
693
899
  generated: undefined;
694
- }, {}, {}>;
695
- menuOptionLabel: drizzle_orm_pg_core.PgColumn<{
696
- name: "menu_option_label";
697
- tableName: "flow_graphs";
900
+ }, {}, {
901
+ length: 64;
902
+ }>;
903
+ source: drizzle_orm_pg_core.PgColumn<{
904
+ name: "source";
905
+ tableName: "documents";
698
906
  dataType: "string";
699
907
  columnType: "PgVarchar";
700
908
  data: string;
701
909
  driverParam: string;
702
- notNull: false;
910
+ notNull: true;
703
911
  hasDefault: false;
704
912
  isPrimaryKey: false;
705
913
  isAutoincrement: false;
@@ -709,11 +917,11 @@ declare const flowGraphs: drizzle_orm_pg_core.PgTableWithColumns<{
709
917
  identity: undefined;
710
918
  generated: undefined;
711
919
  }, {}, {
712
- length: 64;
920
+ length: 12;
713
921
  }>;
714
- createdAt: drizzle_orm_pg_core.PgColumn<{
715
- name: "created_at";
716
- tableName: "flow_graphs";
922
+ linkedAt: drizzle_orm_pg_core.PgColumn<{
923
+ name: "linked_at";
924
+ tableName: "documents";
717
925
  dataType: "date";
718
926
  columnType: "PgTimestamp";
719
927
  data: Date;
@@ -728,16 +936,23 @@ declare const flowGraphs: drizzle_orm_pg_core.PgTableWithColumns<{
728
936
  identity: undefined;
729
937
  generated: undefined;
730
938
  }, {}, {}>;
731
- updatedAt: drizzle_orm_pg_core.PgColumn<{
732
- name: "updated_at";
939
+ };
940
+ dialect: "pg";
941
+ }>;
942
+ declare const flowGraphs: drizzle_orm_pg_core.PgTableWithColumns<{
943
+ name: "flow_graphs";
944
+ schema: "meta_whatsapp";
945
+ columns: {
946
+ id: drizzle_orm_pg_core.PgColumn<{
947
+ name: "id";
733
948
  tableName: "flow_graphs";
734
- dataType: "date";
735
- columnType: "PgTimestamp";
736
- data: Date;
949
+ dataType: "string";
950
+ columnType: "PgUUID";
951
+ data: string;
737
952
  driverParam: string;
738
953
  notNull: true;
739
954
  hasDefault: true;
740
- isPrimaryKey: false;
955
+ isPrimaryKey: true;
741
956
  isAutoincrement: false;
742
957
  hasRuntimeDefault: false;
743
958
  enumValues: undefined;
@@ -745,23 +960,16 @@ declare const flowGraphs: drizzle_orm_pg_core.PgTableWithColumns<{
745
960
  identity: undefined;
746
961
  generated: undefined;
747
962
  }, {}, {}>;
748
- };
749
- dialect: "pg";
750
- }>;
751
- declare const settings: drizzle_orm_pg_core.PgTableWithColumns<{
752
- name: "settings";
753
- schema: "meta_whatsapp";
754
- columns: {
755
963
  companyId: drizzle_orm_pg_core.PgColumn<{
756
964
  name: "company_id";
757
- tableName: "settings";
965
+ tableName: "flow_graphs";
758
966
  dataType: "string";
759
967
  columnType: "PgUUID";
760
968
  data: string;
761
969
  driverParam: string;
762
970
  notNull: true;
763
971
  hasDefault: false;
764
- isPrimaryKey: true;
972
+ isPrimaryKey: false;
765
973
  isAutoincrement: false;
766
974
  hasRuntimeDefault: false;
767
975
  enumValues: undefined;
@@ -769,14 +977,14 @@ declare const settings: drizzle_orm_pg_core.PgTableWithColumns<{
769
977
  identity: undefined;
770
978
  generated: undefined;
771
979
  }, {}, {}>;
772
- templateName: drizzle_orm_pg_core.PgColumn<{
773
- name: "template_name";
774
- tableName: "settings";
980
+ key: drizzle_orm_pg_core.PgColumn<{
981
+ name: "key";
982
+ tableName: "flow_graphs";
775
983
  dataType: "string";
776
984
  columnType: "PgVarchar";
777
985
  data: string;
778
986
  driverParam: string;
779
- notNull: false;
987
+ notNull: true;
780
988
  hasDefault: false;
781
989
  isPrimaryKey: false;
782
990
  isAutoincrement: false;
@@ -786,17 +994,17 @@ declare const settings: drizzle_orm_pg_core.PgTableWithColumns<{
786
994
  identity: undefined;
787
995
  generated: undefined;
788
996
  }, {}, {
789
- length: 128;
997
+ length: 64;
790
998
  }>;
791
- templateLanguage: drizzle_orm_pg_core.PgColumn<{
792
- name: "template_language";
793
- tableName: "settings";
999
+ label: drizzle_orm_pg_core.PgColumn<{
1000
+ name: "label";
1001
+ tableName: "flow_graphs";
794
1002
  dataType: "string";
795
1003
  columnType: "PgVarchar";
796
1004
  data: string;
797
1005
  driverParam: string;
798
1006
  notNull: true;
799
- hasDefault: true;
1007
+ hasDefault: false;
800
1008
  isPrimaryKey: false;
801
1009
  isAutoincrement: false;
802
1010
  hasRuntimeDefault: false;
@@ -805,50 +1013,522 @@ declare const settings: drizzle_orm_pg_core.PgTableWithColumns<{
805
1013
  identity: undefined;
806
1014
  generated: undefined;
807
1015
  }, {}, {
808
- length: 16;
1016
+ length: 120;
809
1017
  }>;
810
- templateVariables: drizzle_orm_pg_core.PgColumn<{
811
- name: "template_variables";
812
- tableName: "settings";
813
- dataType: "json";
814
- columnType: "PgJsonb";
815
- data: string[];
816
- driverParam: unknown;
817
- notNull: true;
818
- hasDefault: true;
1018
+ startNodeId: drizzle_orm_pg_core.PgColumn<{
1019
+ name: "start_node_id";
1020
+ tableName: "flow_graphs";
1021
+ dataType: "string";
1022
+ columnType: "PgVarchar";
1023
+ data: string;
1024
+ driverParam: string;
1025
+ notNull: true;
1026
+ hasDefault: false;
1027
+ isPrimaryKey: false;
1028
+ isAutoincrement: false;
1029
+ hasRuntimeDefault: false;
1030
+ enumValues: [string, ...string[]];
1031
+ baseColumn: never;
1032
+ identity: undefined;
1033
+ generated: undefined;
1034
+ }, {}, {
1035
+ length: 64;
1036
+ }>;
1037
+ nodes: drizzle_orm_pg_core.PgColumn<{
1038
+ name: "nodes";
1039
+ tableName: "flow_graphs";
1040
+ dataType: "json";
1041
+ columnType: "PgJsonb";
1042
+ data: Record<string, unknown>;
1043
+ driverParam: unknown;
1044
+ notNull: true;
1045
+ hasDefault: true;
1046
+ isPrimaryKey: false;
1047
+ isAutoincrement: false;
1048
+ hasRuntimeDefault: false;
1049
+ enumValues: undefined;
1050
+ baseColumn: never;
1051
+ identity: undefined;
1052
+ generated: undefined;
1053
+ }, {}, {
1054
+ $type: Record<string, unknown>;
1055
+ }>;
1056
+ version: drizzle_orm_pg_core.PgColumn<{
1057
+ name: "version";
1058
+ tableName: "flow_graphs";
1059
+ dataType: "number";
1060
+ columnType: "PgInteger";
1061
+ data: number;
1062
+ driverParam: string | number;
1063
+ notNull: true;
1064
+ hasDefault: true;
1065
+ isPrimaryKey: false;
1066
+ isAutoincrement: false;
1067
+ hasRuntimeDefault: false;
1068
+ enumValues: undefined;
1069
+ baseColumn: never;
1070
+ identity: undefined;
1071
+ generated: undefined;
1072
+ }, {}, {}>;
1073
+ showInMenu: drizzle_orm_pg_core.PgColumn<{
1074
+ name: "show_in_menu";
1075
+ tableName: "flow_graphs";
1076
+ dataType: "boolean";
1077
+ columnType: "PgBoolean";
1078
+ data: boolean;
1079
+ driverParam: boolean;
1080
+ notNull: true;
1081
+ hasDefault: true;
1082
+ isPrimaryKey: false;
1083
+ isAutoincrement: false;
1084
+ hasRuntimeDefault: false;
1085
+ enumValues: undefined;
1086
+ baseColumn: never;
1087
+ identity: undefined;
1088
+ generated: undefined;
1089
+ }, {}, {}>;
1090
+ menuOptionLabel: drizzle_orm_pg_core.PgColumn<{
1091
+ name: "menu_option_label";
1092
+ tableName: "flow_graphs";
1093
+ dataType: "string";
1094
+ columnType: "PgVarchar";
1095
+ data: string;
1096
+ driverParam: string;
1097
+ notNull: false;
1098
+ hasDefault: false;
1099
+ isPrimaryKey: false;
1100
+ isAutoincrement: false;
1101
+ hasRuntimeDefault: false;
1102
+ enumValues: [string, ...string[]];
1103
+ baseColumn: never;
1104
+ identity: undefined;
1105
+ generated: undefined;
1106
+ }, {}, {
1107
+ length: 64;
1108
+ }>;
1109
+ createdAt: drizzle_orm_pg_core.PgColumn<{
1110
+ name: "created_at";
1111
+ tableName: "flow_graphs";
1112
+ dataType: "date";
1113
+ columnType: "PgTimestamp";
1114
+ data: Date;
1115
+ driverParam: string;
1116
+ notNull: true;
1117
+ hasDefault: true;
1118
+ isPrimaryKey: false;
1119
+ isAutoincrement: false;
1120
+ hasRuntimeDefault: false;
1121
+ enumValues: undefined;
1122
+ baseColumn: never;
1123
+ identity: undefined;
1124
+ generated: undefined;
1125
+ }, {}, {}>;
1126
+ updatedAt: drizzle_orm_pg_core.PgColumn<{
1127
+ name: "updated_at";
1128
+ tableName: "flow_graphs";
1129
+ dataType: "date";
1130
+ columnType: "PgTimestamp";
1131
+ data: Date;
1132
+ driverParam: string;
1133
+ notNull: true;
1134
+ hasDefault: true;
1135
+ isPrimaryKey: false;
1136
+ isAutoincrement: false;
1137
+ hasRuntimeDefault: false;
1138
+ enumValues: undefined;
1139
+ baseColumn: never;
1140
+ identity: undefined;
1141
+ generated: undefined;
1142
+ }, {}, {}>;
1143
+ };
1144
+ dialect: "pg";
1145
+ }>;
1146
+ /**
1147
+ * Biblioteca de mídia do bot: arquivos que um nó `action` de `send_media` dispara ao chegar nele.
1148
+ *
1149
+ * Tabela separada de `documents` de propósito, e não por simetria: `documents` é o acervo DE UMA
1150
+ * CONVERSA (tem `sessionId` obrigatório) e tem índice único em (companyId, uploadId), porque lá o
1151
+ * mesmo binário chegando duas vezes é reentrega de job. Aqui é o oposto — o mesmo arquivo é
1152
+ * enviado para todo cliente que passar pelo nó, então aquele único bloquearia o segundo envio.
1153
+ *
1154
+ * Sem FK para `flow_graphs.id`: o vínculo natural é (companyId, flowKey), que é justamente o
1155
+ * índice único de lá. `nodeId` não tem como ser FK — nós vivem dentro do jsonb `nodes` — então
1156
+ * apagar um nó no editor deixa a linha órfã; quem lista sempre parte de um nó existente, e a
1157
+ * limpeza é passo de aplicação ao salvar o grafo.
1158
+ */
1159
+ declare const flowMedia: drizzle_orm_pg_core.PgTableWithColumns<{
1160
+ name: "flow_media";
1161
+ schema: "meta_whatsapp";
1162
+ columns: {
1163
+ id: drizzle_orm_pg_core.PgColumn<{
1164
+ name: "id";
1165
+ tableName: "flow_media";
1166
+ dataType: "string";
1167
+ columnType: "PgUUID";
1168
+ data: string;
1169
+ driverParam: string;
1170
+ notNull: true;
1171
+ hasDefault: true;
1172
+ isPrimaryKey: true;
1173
+ isAutoincrement: false;
1174
+ hasRuntimeDefault: false;
1175
+ enumValues: undefined;
1176
+ baseColumn: never;
1177
+ identity: undefined;
1178
+ generated: undefined;
1179
+ }, {}, {}>;
1180
+ companyId: drizzle_orm_pg_core.PgColumn<{
1181
+ name: "company_id";
1182
+ tableName: "flow_media";
1183
+ dataType: "string";
1184
+ columnType: "PgUUID";
1185
+ data: string;
1186
+ driverParam: string;
1187
+ notNull: true;
1188
+ hasDefault: false;
1189
+ isPrimaryKey: false;
1190
+ isAutoincrement: false;
1191
+ hasRuntimeDefault: false;
1192
+ enumValues: undefined;
1193
+ baseColumn: never;
1194
+ identity: undefined;
1195
+ generated: undefined;
1196
+ }, {}, {}>;
1197
+ flowKey: drizzle_orm_pg_core.PgColumn<{
1198
+ name: "flow_key";
1199
+ tableName: "flow_media";
1200
+ dataType: "string";
1201
+ columnType: "PgVarchar";
1202
+ data: string;
1203
+ driverParam: string;
1204
+ notNull: true;
1205
+ hasDefault: false;
1206
+ isPrimaryKey: false;
1207
+ isAutoincrement: false;
1208
+ hasRuntimeDefault: false;
1209
+ enumValues: [string, ...string[]];
1210
+ baseColumn: never;
1211
+ identity: undefined;
1212
+ generated: undefined;
1213
+ }, {}, {
1214
+ length: 64;
1215
+ }>;
1216
+ nodeId: drizzle_orm_pg_core.PgColumn<{
1217
+ name: "node_id";
1218
+ tableName: "flow_media";
1219
+ dataType: "string";
1220
+ columnType: "PgVarchar";
1221
+ data: string;
1222
+ driverParam: string;
1223
+ notNull: true;
1224
+ hasDefault: false;
1225
+ isPrimaryKey: false;
1226
+ isAutoincrement: false;
1227
+ hasRuntimeDefault: false;
1228
+ enumValues: [string, ...string[]];
1229
+ baseColumn: never;
1230
+ identity: undefined;
1231
+ generated: undefined;
1232
+ }, {}, {
1233
+ length: 64;
1234
+ }>;
1235
+ uploadId: drizzle_orm_pg_core.PgColumn<{
1236
+ name: "upload_id";
1237
+ tableName: "flow_media";
1238
+ dataType: "string";
1239
+ columnType: "PgVarchar";
1240
+ data: string;
1241
+ driverParam: string;
1242
+ notNull: true;
1243
+ hasDefault: false;
1244
+ isPrimaryKey: false;
1245
+ isAutoincrement: false;
1246
+ hasRuntimeDefault: false;
1247
+ enumValues: [string, ...string[]];
1248
+ baseColumn: never;
1249
+ identity: undefined;
1250
+ generated: undefined;
1251
+ }, {}, {
1252
+ length: 256;
1253
+ }>;
1254
+ filename: drizzle_orm_pg_core.PgColumn<{
1255
+ name: "filename";
1256
+ tableName: "flow_media";
1257
+ dataType: "string";
1258
+ columnType: "PgVarchar";
1259
+ data: string;
1260
+ driverParam: string;
1261
+ notNull: true;
1262
+ hasDefault: false;
1263
+ isPrimaryKey: false;
1264
+ isAutoincrement: false;
1265
+ hasRuntimeDefault: false;
1266
+ enumValues: [string, ...string[]];
1267
+ baseColumn: never;
1268
+ identity: undefined;
1269
+ generated: undefined;
1270
+ }, {}, {
1271
+ length: 512;
1272
+ }>;
1273
+ mimeType: drizzle_orm_pg_core.PgColumn<{
1274
+ name: "mime_type";
1275
+ tableName: "flow_media";
1276
+ dataType: "string";
1277
+ columnType: "PgVarchar";
1278
+ data: string;
1279
+ driverParam: string;
1280
+ notNull: true;
1281
+ hasDefault: false;
1282
+ isPrimaryKey: false;
1283
+ isAutoincrement: false;
1284
+ hasRuntimeDefault: false;
1285
+ enumValues: [string, ...string[]];
1286
+ baseColumn: never;
1287
+ identity: undefined;
1288
+ generated: undefined;
1289
+ }, {}, {
1290
+ length: 128;
1291
+ }>;
1292
+ sizeBytes: drizzle_orm_pg_core.PgColumn<{
1293
+ name: "size_bytes";
1294
+ tableName: "flow_media";
1295
+ dataType: "number";
1296
+ columnType: "PgInteger";
1297
+ data: number;
1298
+ driverParam: string | number;
1299
+ notNull: true;
1300
+ hasDefault: false;
1301
+ isPrimaryKey: false;
1302
+ isAutoincrement: false;
1303
+ hasRuntimeDefault: false;
1304
+ enumValues: undefined;
1305
+ baseColumn: never;
1306
+ identity: undefined;
1307
+ generated: undefined;
1308
+ }, {}, {}>;
1309
+ caption: drizzle_orm_pg_core.PgColumn<{
1310
+ name: "caption";
1311
+ tableName: "flow_media";
1312
+ dataType: "string";
1313
+ columnType: "PgText";
1314
+ data: string;
1315
+ driverParam: string;
1316
+ notNull: false;
1317
+ hasDefault: false;
1318
+ isPrimaryKey: false;
1319
+ isAutoincrement: false;
1320
+ hasRuntimeDefault: false;
1321
+ enumValues: [string, ...string[]];
1322
+ baseColumn: never;
1323
+ identity: undefined;
1324
+ generated: undefined;
1325
+ }, {}, {}>;
1326
+ sortOrder: drizzle_orm_pg_core.PgColumn<{
1327
+ name: "sort_order";
1328
+ tableName: "flow_media";
1329
+ dataType: "number";
1330
+ columnType: "PgInteger";
1331
+ data: number;
1332
+ driverParam: string | number;
1333
+ notNull: true;
1334
+ hasDefault: true;
1335
+ isPrimaryKey: false;
1336
+ isAutoincrement: false;
1337
+ hasRuntimeDefault: false;
1338
+ enumValues: undefined;
1339
+ baseColumn: never;
1340
+ identity: undefined;
1341
+ generated: undefined;
1342
+ }, {}, {}>;
1343
+ active: drizzle_orm_pg_core.PgColumn<{
1344
+ name: "active";
1345
+ tableName: "flow_media";
1346
+ dataType: "boolean";
1347
+ columnType: "PgBoolean";
1348
+ data: boolean;
1349
+ driverParam: boolean;
1350
+ notNull: true;
1351
+ hasDefault: true;
1352
+ isPrimaryKey: false;
1353
+ isAutoincrement: false;
1354
+ hasRuntimeDefault: false;
1355
+ enumValues: undefined;
1356
+ baseColumn: never;
1357
+ identity: undefined;
1358
+ generated: undefined;
1359
+ }, {}, {}>;
1360
+ createdAt: drizzle_orm_pg_core.PgColumn<{
1361
+ name: "created_at";
1362
+ tableName: "flow_media";
1363
+ dataType: "date";
1364
+ columnType: "PgTimestamp";
1365
+ data: Date;
1366
+ driverParam: string;
1367
+ notNull: true;
1368
+ hasDefault: true;
1369
+ isPrimaryKey: false;
1370
+ isAutoincrement: false;
1371
+ hasRuntimeDefault: false;
1372
+ enumValues: undefined;
1373
+ baseColumn: never;
1374
+ identity: undefined;
1375
+ generated: undefined;
1376
+ }, {}, {}>;
1377
+ updatedAt: drizzle_orm_pg_core.PgColumn<{
1378
+ name: "updated_at";
1379
+ tableName: "flow_media";
1380
+ dataType: "date";
1381
+ columnType: "PgTimestamp";
1382
+ data: Date;
1383
+ driverParam: string;
1384
+ notNull: true;
1385
+ hasDefault: true;
1386
+ isPrimaryKey: false;
1387
+ isAutoincrement: false;
1388
+ hasRuntimeDefault: false;
1389
+ enumValues: undefined;
1390
+ baseColumn: never;
1391
+ identity: undefined;
1392
+ generated: undefined;
1393
+ }, {}, {}>;
1394
+ };
1395
+ dialect: "pg";
1396
+ }>;
1397
+ declare const settings: drizzle_orm_pg_core.PgTableWithColumns<{
1398
+ name: "settings";
1399
+ schema: "meta_whatsapp";
1400
+ columns: {
1401
+ companyId: drizzle_orm_pg_core.PgColumn<{
1402
+ name: "company_id";
1403
+ tableName: "settings";
1404
+ dataType: "string";
1405
+ columnType: "PgUUID";
1406
+ data: string;
1407
+ driverParam: string;
1408
+ notNull: true;
1409
+ hasDefault: false;
1410
+ isPrimaryKey: true;
1411
+ isAutoincrement: false;
1412
+ hasRuntimeDefault: false;
1413
+ enumValues: undefined;
1414
+ baseColumn: never;
1415
+ identity: undefined;
1416
+ generated: undefined;
1417
+ }, {}, {}>;
1418
+ templateName: drizzle_orm_pg_core.PgColumn<{
1419
+ name: "template_name";
1420
+ tableName: "settings";
1421
+ dataType: "string";
1422
+ columnType: "PgVarchar";
1423
+ data: string;
1424
+ driverParam: string;
1425
+ notNull: false;
1426
+ hasDefault: false;
1427
+ isPrimaryKey: false;
1428
+ isAutoincrement: false;
1429
+ hasRuntimeDefault: false;
1430
+ enumValues: [string, ...string[]];
1431
+ baseColumn: never;
1432
+ identity: undefined;
1433
+ generated: undefined;
1434
+ }, {}, {
1435
+ length: 128;
1436
+ }>;
1437
+ templateLanguage: drizzle_orm_pg_core.PgColumn<{
1438
+ name: "template_language";
1439
+ tableName: "settings";
1440
+ dataType: "string";
1441
+ columnType: "PgVarchar";
1442
+ data: string;
1443
+ driverParam: string;
1444
+ notNull: true;
1445
+ hasDefault: true;
1446
+ isPrimaryKey: false;
1447
+ isAutoincrement: false;
1448
+ hasRuntimeDefault: false;
1449
+ enumValues: [string, ...string[]];
1450
+ baseColumn: never;
1451
+ identity: undefined;
1452
+ generated: undefined;
1453
+ }, {}, {
1454
+ length: 16;
1455
+ }>;
1456
+ templateVariables: drizzle_orm_pg_core.PgColumn<{
1457
+ name: "template_variables";
1458
+ tableName: "settings";
1459
+ dataType: "json";
1460
+ columnType: "PgJsonb";
1461
+ data: string[];
1462
+ driverParam: unknown;
1463
+ notNull: true;
1464
+ hasDefault: true;
1465
+ isPrimaryKey: false;
1466
+ isAutoincrement: false;
1467
+ hasRuntimeDefault: false;
1468
+ enumValues: undefined;
1469
+ baseColumn: never;
1470
+ identity: undefined;
1471
+ generated: undefined;
1472
+ }, {}, {
1473
+ $type: string[];
1474
+ }>;
1475
+ welcomeMessage: drizzle_orm_pg_core.PgColumn<{
1476
+ name: "welcome_message";
1477
+ tableName: "settings";
1478
+ dataType: "string";
1479
+ columnType: "PgText";
1480
+ data: string;
1481
+ driverParam: string;
1482
+ notNull: false;
1483
+ hasDefault: false;
1484
+ isPrimaryKey: false;
1485
+ isAutoincrement: false;
1486
+ hasRuntimeDefault: false;
1487
+ enumValues: [string, ...string[]];
1488
+ baseColumn: never;
1489
+ identity: undefined;
1490
+ generated: undefined;
1491
+ }, {}, {}>;
1492
+ farewellMessage: drizzle_orm_pg_core.PgColumn<{
1493
+ name: "farewell_message";
1494
+ tableName: "settings";
1495
+ dataType: "string";
1496
+ columnType: "PgText";
1497
+ data: string;
1498
+ driverParam: string;
1499
+ notNull: false;
1500
+ hasDefault: false;
819
1501
  isPrimaryKey: false;
820
1502
  isAutoincrement: false;
821
1503
  hasRuntimeDefault: false;
822
- enumValues: undefined;
1504
+ enumValues: [string, ...string[]];
823
1505
  baseColumn: never;
824
1506
  identity: undefined;
825
1507
  generated: undefined;
826
- }, {}, {
827
- $type: string[];
828
- }>;
829
- welcomeMessage: drizzle_orm_pg_core.PgColumn<{
830
- name: "welcome_message";
1508
+ }, {}, {}>;
1509
+ transcriptionEnabled: drizzle_orm_pg_core.PgColumn<{
1510
+ name: "transcription_enabled";
831
1511
  tableName: "settings";
832
- dataType: "string";
833
- columnType: "PgText";
834
- data: string;
835
- driverParam: string;
1512
+ dataType: "boolean";
1513
+ columnType: "PgBoolean";
1514
+ data: boolean;
1515
+ driverParam: boolean;
836
1516
  notNull: false;
837
1517
  hasDefault: false;
838
1518
  isPrimaryKey: false;
839
1519
  isAutoincrement: false;
840
1520
  hasRuntimeDefault: false;
841
- enumValues: [string, ...string[]];
1521
+ enumValues: undefined;
842
1522
  baseColumn: never;
843
1523
  identity: undefined;
844
1524
  generated: undefined;
845
1525
  }, {}, {}>;
846
- farewellMessage: drizzle_orm_pg_core.PgColumn<{
847
- name: "farewell_message";
1526
+ transcriptionMode: drizzle_orm_pg_core.PgColumn<{
1527
+ name: "transcription_mode";
848
1528
  tableName: "settings";
849
1529
  dataType: "string";
850
- columnType: "PgText";
851
- data: string;
1530
+ columnType: "PgVarchar";
1531
+ data: TranscriptionMode;
852
1532
  driverParam: string;
853
1533
  notNull: false;
854
1534
  hasDefault: false;
@@ -859,7 +1539,10 @@ declare const settings: drizzle_orm_pg_core.PgTableWithColumns<{
859
1539
  baseColumn: never;
860
1540
  identity: undefined;
861
1541
  generated: undefined;
862
- }, {}, {}>;
1542
+ }, {}, {
1543
+ length: 16;
1544
+ $type: TranscriptionMode;
1545
+ }>;
863
1546
  createdAt: drizzle_orm_pg_core.PgColumn<{
864
1547
  name: "created_at";
865
1548
  tableName: "settings";
@@ -905,6 +1588,10 @@ type MessageRow = typeof messages.$inferSelect;
905
1588
  type NewMessageRow = typeof messages.$inferInsert;
906
1589
  type FlowGraphRow = typeof flowGraphs.$inferSelect;
907
1590
  type NewFlowGraphRow = typeof flowGraphs.$inferInsert;
1591
+ type DocumentRow = typeof documents.$inferSelect;
1592
+ type NewDocumentRow = typeof documents.$inferInsert;
1593
+ type FlowMediaRow = typeof flowMedia.$inferSelect;
1594
+ type NewFlowMediaRow = typeof flowMedia.$inferInsert;
908
1595
 
909
1596
  interface ListConversationsFilters {
910
1597
  page?: number;
@@ -917,13 +1604,23 @@ declare class SessionRepository {
917
1604
  constructor(db: MetaWhatsAppDatabase);
918
1605
  getContext(companyId: string, whatsappNumber: string): Promise<SessionRow | undefined>;
919
1606
  getOrCreate(companyId: string, whatsappNumber: string, startState: SessionState): Promise<SessionRow>;
920
- setState(companyId: string, whatsappNumber: string, state: SessionState, context?: Record<string, unknown>): Promise<void>;
1607
+ setState<TSessionContext extends Record<string, unknown> = Record<string, unknown>>(companyId: string, whatsappNumber: string, state: SessionState, context?: TSessionContext): Promise<void>;
1608
+ patchContext<TSessionContext extends Record<string, unknown> = Record<string, unknown>>(companyId: string, whatsappNumber: string, patch: Partial<TSessionContext>): Promise<void>;
1609
+ readContext<TSessionContext extends Record<string, unknown> = Record<string, unknown>>(companyId: string, whatsappNumber: string): Promise<TSessionContext | undefined>;
921
1610
  setFlowPosition(companyId: string, whatsappNumber: string, flowKey: string | null, currentNodeId: string | null): Promise<void>;
922
1611
  touchInbound(companyId: string, whatsappNumber: string): Promise<void>;
923
1612
  hoursSinceLastInbound(companyId: string, whatsappNumber: string): Promise<number | undefined>;
924
1613
  setMode(companyId: string, whatsappNumber: string, mode: SessionMode, assignedUserId?: string | null): Promise<void>;
925
1614
  takeover(companyId: string, whatsappNumber: string, agentUserId: string): Promise<void>;
926
1615
  release(companyId: string, whatsappNumber: string): Promise<void>;
1616
+ /**
1617
+ * Apaga a sessão; a cascata das FKs leva mensagens e documentos.
1618
+ *
1619
+ * Não apaga o binário no storage — isso é passo de aplicação, e é por isso que este método é
1620
+ * chamado por `DeleteConversationUseCase` e não diretamente pelo host. Chamar daqui sem apagar os
1621
+ * objetos antes deixa mídia órfã sendo cobrada para sempre.
1622
+ */
1623
+ deleteByNumber(companyId: string, whatsappNumber: string): Promise<void>;
927
1624
  requestHuman(companyId: string, whatsappNumber: string): Promise<void>;
928
1625
  markRead(companyId: string, whatsappNumber: string): Promise<void>;
929
1626
  markAllRead(companyId: string, userId: string): Promise<number>;
@@ -960,11 +1657,108 @@ declare class SessionRepository {
960
1657
  waMessageId: string | null;
961
1658
  status: string | null;
962
1659
  readAt: Date | null;
1660
+ moderationFlagged: boolean | null;
1661
+ moderationTerms: string[] | null;
1662
+ transcriptionStatus: TranscriptionStatus | null;
1663
+ transcriptionText: string | null;
1664
+ transcriptionLanguage: string | null;
1665
+ transcriptionEngine: string | null;
963
1666
  createdAt: Date;
964
1667
  }[];
965
1668
  } | null>;
966
1669
  }
967
1670
 
1671
+ interface InsertMessageParams {
1672
+ companyId: string;
1673
+ sessionId: string;
1674
+ whatsappNumber: string;
1675
+ direction: MessageDirection;
1676
+ sender: MessageSender;
1677
+ agentUserId?: string | null;
1678
+ type: string;
1679
+ content?: string | null;
1680
+ payload?: Record<string, unknown> | null;
1681
+ waMessageId?: string | null;
1682
+ status?: MessageStatus | null;
1683
+ /** `undefined` deixa a coluna nula: não avaliado, distinto de avaliado e limpo. */
1684
+ moderationFlagged?: boolean | null;
1685
+ moderationTerms?: string[] | null;
1686
+ }
1687
+ interface ListMessagesParams$1 {
1688
+ companyId: string;
1689
+ sessionId: string;
1690
+ limit?: number;
1691
+ before?: string;
1692
+ }
1693
+ interface SaveTranscriptionByWaMessageIdParams extends Omit<SaveTranscriptionParams, 'messageId'> {
1694
+ /**
1695
+ * Id da mensagem na Meta. É o único que quem processa o webhook conhece — o id do módulo só
1696
+ * existe depois da gravação, e obrigar o host a descobri-lo faria cada um escrever a própria
1697
+ * consulta por `wa_message_id`.
1698
+ */
1699
+ waMessageId: string;
1700
+ }
1701
+ interface SaveTranscriptionParams {
1702
+ companyId: string;
1703
+ messageId: string;
1704
+ status: TranscriptionStatus;
1705
+ /** Ausente em `pending`/`failed`/`unsupported`; vazio em `done` é silêncio já processado. */
1706
+ text?: string | null;
1707
+ language?: string | null;
1708
+ engine?: string | null;
1709
+ }
1710
+ declare class MessageRepository {
1711
+ private readonly db;
1712
+ constructor(db: MetaWhatsAppDatabase);
1713
+ insertMessage(params: InsertMessageParams): Promise<MessageRow | undefined>;
1714
+ updateMessageStatus(companyId: string, waMessageId: string, status: MessageStatus): Promise<MessageRow | undefined>;
1715
+ /**
1716
+ * Grava a transcrição endereçando pelo id da Meta, para quem só tem esse.
1717
+ *
1718
+ * Serve ao caso em que a transcrição acontece no próprio webhook — o grafo precisa do texto para
1719
+ * responder ao cliente, e jogar fora o que ele já pagou para transcrever significaria transcrever
1720
+ * o mesmo áudio uma segunda vez só para o painel ver.
1721
+ *
1722
+ * Devolve `undefined` quando não achou a mensagem: entrega duplicada e mensagem apagada são
1723
+ * corridas normais, não erro.
1724
+ */
1725
+ saveTranscriptionByWaMessageId(params: SaveTranscriptionByWaMessageIdParams): Promise<MessageRow | undefined>;
1726
+ findById(companyId: string, messageId: string): Promise<MessageRow | undefined>;
1727
+ /**
1728
+ * Grava o resultado da transcrição. Devolve `undefined` quando a mensagem não existe (apagada
1729
+ * entre o enfileiramento e a execução do job) — não é erro, é corrida normal.
1730
+ *
1731
+ * `text`/`language`/`engine` só são tocados quando informados: uma retentativa que volta a falhar
1732
+ * atualiza o status sem apagar a transcrição parcial de uma tentativa anterior que tenha vindo de
1733
+ * outro engine da cadeia.
1734
+ */
1735
+ saveTranscription(params: SaveTranscriptionParams): Promise<MessageRow | undefined>;
1736
+ listByConversation(params: ListMessagesParams$1): Promise<MessageRow[]>;
1737
+ }
1738
+
1739
+ declare const DEFAULT_FLOW_GRAPH_CACHE_TTL_SECONDS = 300;
1740
+ /**
1741
+ * Cache de leitura dos grafos de fluxo, com invalidação na publicação.
1742
+ *
1743
+ * TTL **e** invalidação explícita, e não um dos dois: só TTL deixaria o cliente andando no grafo
1744
+ * antigo até a chave expirar, logo depois de alguém corrigir o fluxo no editor; só invalidação
1745
+ * deixaria cache envenenado para sempre se um `delete` se perdesse (Redis reiniciando, deploy no
1746
+ * meio da escrita).
1747
+ *
1748
+ * Toda operação é tolerante a falha por decisão de projeto: cache é aceleração, não dependência.
1749
+ * Redis fora do ar tem que degradar para leitura no banco — que é exatamente o comportamento de
1750
+ * quem não configura cache nenhum — em vez de derrubar a conversa do cliente.
1751
+ */
1752
+ declare class FlowGraphCache {
1753
+ private readonly provider;
1754
+ private readonly ttlSeconds;
1755
+ constructor(provider: CacheInterface, ttlSeconds?: number);
1756
+ private keyFor;
1757
+ read(companyId: string, flowKey: string): Promise<FlowGraphData | undefined>;
1758
+ write(companyId: string, graph: FlowGraphData): Promise<void>;
1759
+ invalidate(companyId: string, flowKey: string): Promise<void>;
1760
+ }
1761
+
968
1762
  declare class InvalidFlowGraphError extends Error {
969
1763
  readonly validationMessage: string;
970
1764
  constructor(key: string, validationMessage: string);
@@ -974,7 +1768,8 @@ declare class OptimisticLockError extends Error {
974
1768
  }
975
1769
  declare class FlowGraphRepository {
976
1770
  private readonly db;
977
- constructor(db: MetaWhatsAppDatabase);
1771
+ private readonly cache?;
1772
+ constructor(db: MetaWhatsAppDatabase, cache?: FlowGraphCache | undefined);
978
1773
  get(companyId: string, key: string): Promise<FlowGraphData | undefined>;
979
1774
  list(companyId: string): Promise<FlowGraphSummary[]>;
980
1775
  private assertValidNodes;
@@ -995,42 +1790,124 @@ declare class SettingsRepository {
995
1790
  resolveTemplateVariables(companyId: string, context: Record<string, unknown>): Promise<string[]>;
996
1791
  }
997
1792
 
998
- interface InsertMessageParams {
1793
+ type LogMessageParams = Omit<InsertMessageParams, 'sessionId'> & {
1794
+ startState: SessionState;
1795
+ };
1796
+ /**
1797
+ * O contrato mínimo de `@adatechnology/text-moderation`, declarado aqui em vez de importado.
1798
+ *
1799
+ * Assim o módulo não ganha dependência de pacote para um recurso opcional, e — mais importante —
1800
+ * moderação não fica amarrada a WhatsApp: o que atravessa esta fronteira é texto, e qualquer canal
1801
+ * que passe a escrever no transcript por este use case herda a marcação sem tocar nada aqui.
1802
+ */
1803
+ type MessageModerator = {
1804
+ inspect: (text: string) => {
1805
+ isOffensive: boolean;
1806
+ matchedTerms: readonly string[];
1807
+ };
1808
+ };
1809
+ declare class LogMessageUseCase {
1810
+ private readonly sessionRepository;
1811
+ private readonly messageRepository;
1812
+ private readonly realtime?;
1813
+ private readonly moderator?;
1814
+ constructor(sessionRepository: SessionRepository, messageRepository: MessageRepository, realtime?: RealtimeNotifierInterface | undefined, moderator?: MessageModerator | undefined);
1815
+ execute(params: LogMessageParams): Promise<MessageRow | undefined>;
1816
+ private moderationOf;
1817
+ }
1818
+
1819
+ interface LinkDocumentParams {
999
1820
  companyId: string;
1000
1821
  sessionId: string;
1001
- whatsappNumber: string;
1002
- direction: MessageDirection;
1003
- sender: MessageSender;
1004
- agentUserId?: string | null;
1005
- type: string;
1006
- content?: string | null;
1007
- payload?: Record<string, unknown> | null;
1008
- waMessageId?: string | null;
1009
- status?: MessageStatus | null;
1822
+ messageId?: string | null;
1823
+ uploadId: string;
1824
+ filename: string;
1825
+ mimeType: string;
1826
+ sizeBytes: number;
1827
+ sha256?: string | null;
1828
+ source: string;
1010
1829
  }
1011
- interface ListMessagesParams$1 {
1830
+ interface ListDocumentsParams {
1012
1831
  companyId: string;
1013
1832
  sessionId: string;
1833
+ search?: string;
1834
+ /**
1835
+ * Origens aceitas, como lista explícita (`['agent', 'bot']`) e não como apelido de UI.
1836
+ *
1837
+ * Agrupamento tipo "Equipe" é vocabulário de tela e muda por produto; traduzir aqui obrigaria o
1838
+ * módulo a conhecer o rótulo de cada host. Quem recebe o apelido na borda é a rota.
1839
+ */
1840
+ sources?: readonly string[];
1841
+ sortDirection?: 'asc' | 'desc';
1842
+ page?: number;
1014
1843
  limit?: number;
1015
- before?: string;
1016
1844
  }
1017
- declare class MessageRepository {
1845
+ interface ListCompanyDocumentsParams$1 {
1846
+ companyId: string;
1847
+ search?: string;
1848
+ sources?: readonly string[];
1849
+ sortDirection?: 'asc' | 'desc';
1850
+ page?: number;
1851
+ limit?: number;
1852
+ }
1853
+ interface CompanyDocumentRow {
1854
+ id: string;
1855
+ uploadId: string;
1856
+ filename: string;
1857
+ mimeType: string;
1858
+ sizeBytes: number;
1859
+ source: string;
1860
+ linkedAt: Date;
1861
+ whatsappNumber: string;
1862
+ }
1863
+ interface ListCompanyDocumentsResult {
1864
+ rows: CompanyDocumentRow[];
1865
+ total: number;
1866
+ }
1867
+ interface ListDocumentsResult {
1868
+ rows: DocumentRow[];
1869
+ /** Total no servidor, ANTES do corte de página — é o que permite calcular a última página. */
1870
+ total: number;
1871
+ }
1872
+ declare class DocumentRepository {
1018
1873
  private readonly db;
1019
1874
  constructor(db: MetaWhatsAppDatabase);
1020
- insertMessage(params: InsertMessageParams): Promise<MessageRow | undefined>;
1021
- updateMessageStatus(companyId: string, waMessageId: string, status: MessageStatus): Promise<MessageRow | undefined>;
1022
- listByConversation(params: ListMessagesParams$1): Promise<MessageRow[]>;
1023
- }
1024
-
1025
- type LogMessageParams = Omit<InsertMessageParams, 'sessionId'> & {
1026
- startState: SessionState;
1027
- };
1028
- declare class LogMessageUseCase {
1029
- private readonly sessionRepository;
1030
- private readonly messageRepository;
1031
- private readonly realtime?;
1032
- constructor(sessionRepository: SessionRepository, messageRepository: MessageRepository, realtime?: RealtimeNotifierInterface | undefined);
1033
- execute(params: LogMessageParams): Promise<MessageRow | undefined>;
1875
+ /**
1876
+ * Idempotente por (companyId, uploadId), garantido pelo índice único e não por SELECT prévio: o
1877
+ * job de ingestão é reentregue por retry e duas tentativas concorrentes passariam as duas por uma
1878
+ * checagem, duplicando a linha no painel.
1879
+ *
1880
+ * Devolve `undefined` quando o documento já estava linkado.
1881
+ */
1882
+ link(params: LinkDocumentParams): Promise<DocumentRow | undefined>;
1883
+ listByConversation(params: ListDocumentsParams): Promise<ListDocumentsResult>;
1884
+ /**
1885
+ * A biblioteca da EMPRESA inteira, não de uma conversa.
1886
+ *
1887
+ * A busca casa nome do arquivo OU telefone da conversa — ver `companyDocumentSearch`.
1888
+ *
1889
+ * Faz join com `sessions` para carregar de qual conversa cada arquivo veio — numa lista global,
1890
+ * arquivo sem essa referência é inútil: o atendente vê "comprovante.pdf" e não sabe de quem.
1891
+ *
1892
+ * Ordena por `linkedAt` apoiada no índice `idx_documents_company_linked`, que já existia para a
1893
+ * varredura de retenção.
1894
+ */
1895
+ listByCompany(params: ListCompanyDocumentsParams$1): Promise<ListCompanyDocumentsResult>;
1896
+ /**
1897
+ * Um documento pela key do objeto. Serve para recuperar o nome original na hora de assinar o
1898
+ * download: a key é caminho no bucket e salvaria o arquivo com o id da Meta.
1899
+ */
1900
+ findByUploadId(companyId: string, uploadId: string): Promise<DocumentRow | undefined>;
1901
+ /**
1902
+ * Os objetos a apagar no storage antes de a linha sumir.
1903
+ *
1904
+ * Existe porque a cascata da FK apaga a linha e deixa o binário órfão: quem for apagar a conversa
1905
+ * precisa desta lista primeiro, senão paga armazenamento para sempre por arquivo inalcançável.
1906
+ */
1907
+ listUploadIdsBySession(companyId: string, sessionId: string): Promise<string[]>;
1908
+ /** Varredura de retenção por idade — o par é o mesmo cuidado com o objeto no storage. */
1909
+ listExpired(companyId: string, olderThan: Date, limit?: number): Promise<DocumentRow[]>;
1910
+ deleteById(companyId: string, id: string): Promise<void>;
1034
1911
  }
1035
1912
 
1036
1913
  type SendTextParams = {
@@ -1067,7 +1944,8 @@ declare class SendMessageUseCase {
1067
1944
  private readonly sessionRepository;
1068
1945
  private readonly logMessage;
1069
1946
  private readonly objectStorage?;
1070
- constructor(channel: ChannelAdapterInterface, sessionRepository: SessionRepository, logMessage: LogMessageUseCase, objectStorage?: ObjectStorageInterface | undefined);
1947
+ private readonly documentRepository?;
1948
+ constructor(channel: ChannelAdapterInterface, sessionRepository: SessionRepository, logMessage: LogMessageUseCase, objectStorage?: ObjectStorageInterface | undefined, documentRepository?: DocumentRepository | undefined);
1071
1949
  private assertWithinWindow;
1072
1950
  sendText(params: SendTextParams): Promise<MessageRow | undefined>;
1073
1951
  sendMedia(params: SendMediaParams): Promise<MessageRow | undefined>;
@@ -1120,6 +1998,143 @@ declare class ListMessagesUseCase {
1120
1998
  execute(params: ListMessagesParams): Promise<MessageRow[]>;
1121
1999
  }
1122
2000
 
2001
+ type ListConversationDocumentsParams = {
2002
+ companyId: string;
2003
+ whatsappNumber: string;
2004
+ search?: string;
2005
+ /** Origens explícitas. O apelido de UI ("Equipe") é traduzido na borda HTTP, não aqui. */
2006
+ sources?: readonly string[];
2007
+ sortDirection?: 'asc' | 'desc';
2008
+ page?: number;
2009
+ limit?: number;
2010
+ };
2011
+ /**
2012
+ * O que o `ConversationDocumentsPanel` do conversations-ui consome. O shape é o do pacote de UI
2013
+ * (`ConversationDocument`), com `linkedAt` já em ISO para não obrigar cada host a serializar.
2014
+ */
2015
+ type ConversationDocumentView = {
2016
+ /**
2017
+ * O `uploadId` (key no storage), e NÃO o id da linha.
2018
+ *
2019
+ * É este valor que o consumidor devolve para pedir a URL assinada ou montar o zip, então expor o
2020
+ * UUID da tabela aqui fazia o download apontar para um objeto inexistente — falha que só aparece
2021
+ * no clique, porque a assinatura é gerada sem consultar o bucket.
2022
+ */
2023
+ id: string;
2024
+ filename: string;
2025
+ mimeType: string;
2026
+ sizeBytes: number;
2027
+ source: string;
2028
+ linkedAt: string;
2029
+ };
2030
+ /** Espelha o `ConversationDocumentPage` do conversations-ui: lista da página + total no servidor. */
2031
+ type ConversationDocumentsPage = {
2032
+ documents: ConversationDocumentView[];
2033
+ total: number;
2034
+ };
2035
+ declare class ListConversationDocumentsUseCase {
2036
+ private readonly sessionRepository;
2037
+ private readonly documentRepository;
2038
+ constructor(sessionRepository: SessionRepository, documentRepository: DocumentRepository);
2039
+ execute(params: ListConversationDocumentsParams): Promise<ConversationDocumentsPage>;
2040
+ }
2041
+
2042
+ type ListCompanyDocumentsParams = {
2043
+ companyId: string;
2044
+ search?: string;
2045
+ /** Origens explícitas. O apelido de UI ("Equipe") é traduzido na borda HTTP, não aqui. */
2046
+ sources?: readonly string[];
2047
+ sortDirection?: 'asc' | 'desc';
2048
+ page?: number;
2049
+ limit?: number;
2050
+ };
2051
+ /**
2052
+ * Um arquivo na biblioteca da empresa. Igual ao da conversa, mais `conversationId` — sem saber de
2053
+ * quem veio, uma lista global de anexos não responde nenhuma pergunta útil.
2054
+ */
2055
+ type CompanyDocumentView = {
2056
+ /** O `uploadId` (key no storage), pelo mesmo motivo do `ConversationDocumentView`. */
2057
+ id: string;
2058
+ conversationId: string;
2059
+ filename: string;
2060
+ mimeType: string;
2061
+ sizeBytes: number;
2062
+ source: string;
2063
+ linkedAt: string;
2064
+ };
2065
+ type CompanyDocumentsPage = {
2066
+ documents: CompanyDocumentView[];
2067
+ total: number;
2068
+ };
2069
+ /**
2070
+ * Biblioteca de arquivos de todas as conversas da empresa.
2071
+ *
2072
+ * Existe separada de `ListConversationDocumentsUseCase` porque a pergunta é outra: aquela parte de
2073
+ * uma conversa conhecida, esta varre a empresa e por isso precisa dizer de qual conversa cada
2074
+ * arquivo veio. Reaproveitar a primeira exigiria um `sessionId` opcional que muda o significado do
2075
+ * retorno — dois nomes claros custam menos que um parâmetro que dobra o comportamento.
2076
+ */
2077
+ declare class ListCompanyDocumentsUseCase {
2078
+ private readonly documentRepository;
2079
+ constructor(documentRepository: DocumentRepository);
2080
+ execute(params: ListCompanyDocumentsParams): Promise<CompanyDocumentsPage>;
2081
+ }
2082
+
2083
+ type DeleteConversationParams = {
2084
+ companyId: string;
2085
+ whatsappNumber: string;
2086
+ };
2087
+ type DeleteConversationResult = {
2088
+ /** Objetos efetivamente apagados no storage. */
2089
+ deletedObjects: number;
2090
+ /** Objetos que o storage recusou. A conversa NÃO é apagada quando isto é maior que zero. */
2091
+ failedObjects: readonly string[];
2092
+ };
2093
+ /**
2094
+ * Apaga a conversa e a mídia dela.
2095
+ *
2096
+ * A ordem é o ponto: **storage primeiro, banco depois**. A FK de `documents.session_id` é
2097
+ * `on delete cascade`, então apagar a sessão primeiro derrubaria as linhas e levaria embora a única
2098
+ * lista de `uploadId` existente — os binários ficariam órfãos, cobrados para sempre e inalcançáveis.
2099
+ *
2100
+ * Se algum objeto falhar, a conversa é preservada e o chamador recebe a lista. Apagar as linhas
2101
+ * "mesmo assim" transformaria uma falha visível e reexecutável em lixo silencioso no storage.
2102
+ */
2103
+ declare class DeleteConversationUseCase {
2104
+ private readonly sessionRepository;
2105
+ private readonly documentRepository;
2106
+ private readonly objectStorage?;
2107
+ constructor(sessionRepository: SessionRepository, documentRepository: DocumentRepository, objectStorage?: ObjectStorageInterface | undefined);
2108
+ execute(params: DeleteConversationParams): Promise<DeleteConversationResult>;
2109
+ }
2110
+
2111
+ type PurgeExpiredDocumentsParams = {
2112
+ companyId: string;
2113
+ /** Dias de retenção. O produto configura; o módulo não escolhe política de dado pessoal. */
2114
+ retentionDays: number;
2115
+ /** Teto por execução, para o job não segurar conexão nem storage por tempo indefinido. */
2116
+ batchSize?: number;
2117
+ /** Instante de referência — injetado para o teste não depender do relógio. */
2118
+ now?: Date;
2119
+ };
2120
+ type PurgeExpiredDocumentsResult = {
2121
+ purged: number;
2122
+ failed: readonly string[];
2123
+ };
2124
+ /**
2125
+ * Apaga documento vencido: objeto no storage primeiro, linha depois — a mesma ordem do
2126
+ * `DeleteConversationUseCase`, e pelo mesmo motivo.
2127
+ *
2128
+ * A linha só cai quando o objeto caiu. Contar como purgado sem ter apagado o binário deixaria lixo
2129
+ * pago no storage sem nenhum registro que o alcance depois: a linha era o único ponteiro.
2130
+ */
2131
+ declare class PurgeExpiredDocumentsUseCase {
2132
+ private readonly documentRepository;
2133
+ private readonly objectStorage?;
2134
+ constructor(documentRepository: DocumentRepository, objectStorage?: ObjectStorageInterface | undefined);
2135
+ execute(params: PurgeExpiredDocumentsParams): Promise<PurgeExpiredDocumentsResult>;
2136
+ }
2137
+
1123
2138
  type ExportConversationParams = {
1124
2139
  companyId: string;
1125
2140
  whatsappNumber: string;
@@ -1241,10 +2256,77 @@ declare class FlowInterpreter {
1241
2256
  }): Promise<FlowRunResult>;
1242
2257
  }
1243
2258
 
2259
+ type FlowMediaLocation = {
2260
+ companyId: string;
2261
+ flowKey: string;
2262
+ nodeId: string;
2263
+ };
2264
+ type AttachFlowMediaParams = FlowMediaLocation & {
2265
+ uploadId: string;
2266
+ filename: string;
2267
+ mimeType: string;
2268
+ sizeBytes: number;
2269
+ caption?: string;
2270
+ sortOrder?: number;
2271
+ };
2272
+ type UpdateFlowMediaParams = {
2273
+ companyId: string;
2274
+ id: string;
2275
+ caption?: string | null;
2276
+ sortOrder?: number;
2277
+ active?: boolean;
2278
+ };
2279
+ declare class FlowMediaRepository {
2280
+ private readonly db;
2281
+ constructor(db: MetaWhatsAppDatabase);
2282
+ private locationFilter;
2283
+ listActive(location: FlowMediaLocation): Promise<FlowMediaRow[]>;
2284
+ listAll(location: FlowMediaLocation): Promise<FlowMediaRow[]>;
2285
+ /**
2286
+ * Anexa um arquivo já existente no storage ao nó.
2287
+ *
2288
+ * `onConflictDoUpdate` em vez de deixar estourar: reanexar o mesmo arquivo é clique repetido no
2289
+ * editor, e o esperado ali é atualizar a legenda/ordem — não um erro de índice único na cara de
2290
+ * quem está montando o fluxo.
2291
+ */
2292
+ attach(params: AttachFlowMediaParams): Promise<FlowMediaRow>;
2293
+ update(params: UpdateFlowMediaParams): Promise<FlowMediaRow | undefined>;
2294
+ /**
2295
+ * Desanexa do nó. Não toca no storage de propósito: o mesmo `uploadId` pode estar anexado a
2296
+ * outro nó ou a outro fluxo, e apagar o binário aqui quebraria os demais. Quem apaga objeto é o
2297
+ * host, que é dono da biblioteca de arquivos.
2298
+ */
2299
+ detach(params: {
2300
+ companyId: string;
2301
+ id: string;
2302
+ }): Promise<void>;
2303
+ detachRemovedNodes(params: {
2304
+ companyId: string;
2305
+ flowKey: string;
2306
+ existingNodeIds: string[];
2307
+ }): Promise<void>;
2308
+ }
2309
+
2310
+ /**
2311
+ * Copyright (c) 2026 Ada Technology. MIT License.
2312
+ *
2313
+ * O algoritmo mora em `meta-graph-core`, compartilhado com os outros objetos que a Meta assina.
2314
+ * Aqui fica só a tradução para o vocabulário deste domínio: o erro lançado e o namespace do nonce.
2315
+ */
2316
+
1244
2317
  interface NonceStoreInterface {
1245
2318
  setIfAbsent(key: string, ttlSeconds: number): Promise<boolean>;
2319
+ /**
2320
+ * Estende a chave já reivindicada para a janela cheia. Um SET simples, sem NX.
2321
+ *
2322
+ * Opcional só por compatibilidade com hosts que ainda não o implementam: sem ele o claim curto
2323
+ * expira, e a Meta pode reentregar uma entrega já processada — trabalho repetido, que a dedupe
2324
+ * por `waMessageId` ainda segura antes de virar efeito visível ao cliente. Implementar é o
2325
+ * caminho correto.
2326
+ */
2327
+ confirm?(key: string, ttlSeconds: number): Promise<void>;
1246
2328
  }
1247
- declare const WEBHOOK_NONCE_TTL_SECONDS = 300;
2329
+
1248
2330
  declare function verifyWebhookChallenge(params: {
1249
2331
  mode: string | null;
1250
2332
  token: string | null;
@@ -1256,11 +2338,101 @@ declare function verifyWebhookSignature(params: {
1256
2338
  signatureHeader: string | null | undefined;
1257
2339
  appSecret: string;
1258
2340
  }): void;
2341
+ /**
2342
+ * Reivindica a entrega por pouco tempo. O par obrigatório é `confirmWebhookDelivery` ao fim do
2343
+ * processamento — ver `WEBHOOK_CLAIM_TTL_SECONDS` para o porquê dos dois tempos.
2344
+ */
1259
2345
  declare function claimWebhookDelivery(params: {
1260
2346
  nonceStore: NonceStoreInterface;
1261
2347
  signatureHeader: string;
1262
2348
  ttlSeconds?: number;
1263
2349
  }): Promise<boolean>;
2350
+ /** Só depois da entrega processada por inteiro: é isto que fecha a janela anti-replay. */
2351
+ declare function confirmWebhookDelivery(params: {
2352
+ nonceStore: NonceStoreInterface;
2353
+ signatureHeader: string;
2354
+ ttlSeconds?: number;
2355
+ }): Promise<void>;
2356
+
2357
+ /**
2358
+ * Copyright (c) 2026 Ada Technology. MIT License.
2359
+ *
2360
+ * Separa a entrega do webhook em duas metades: o que precisa acontecer antes de responder à Meta
2361
+ * (persistir a mensagem, que é escrita local e rápida) e o que pode esperar alguns milissegundos
2362
+ * (os hooks do host, que costumam falar com rede — motor de fluxo, IA, integrações).
2363
+ *
2364
+ * A segunda metade rodava dentro da requisição do webhook. Um deploy no meio de uma conversa
2365
+ * matava a chamada em voo, e o cliente ficava sem resposta sem que nada registrasse a perda. Com
2366
+ * uma fila no host, derrubar o processo vira atraso: o job espera e é retomado.
2367
+ */
2368
+
2369
+ type InboundMediaDescriptor = {
2370
+ sourceMediaId: string;
2371
+ mimeType: string;
2372
+ filename?: string;
2373
+ };
2374
+ type InboundMessageEffectsJob = {
2375
+ kind: 'message';
2376
+ companyId: string;
2377
+ message: WhatsAppMessage;
2378
+ savedMessageId: string;
2379
+ media?: InboundMediaDescriptor;
2380
+ receivedAt: number;
2381
+ };
2382
+ type InboundStatusEffectsJob = {
2383
+ kind: 'status';
2384
+ companyId: string;
2385
+ status: WhatsAppStatus;
2386
+ whatsappNumber: string;
2387
+ receivedAt: number;
2388
+ };
2389
+ type InboundDispatchJob = InboundMessageEffectsJob | InboundStatusEffectsJob;
2390
+ /**
2391
+ * Porta de fila. O módulo não escolhe a tecnologia — BullMQ, SQS, o que o host já tiver — mas
2392
+ * exige dela duas garantias, e sem as duas o padrão não entrega o que promete:
2393
+ *
2394
+ * 1. **Durabilidade**: o job sobrevive à morte do processo que o enfileirou. Uma fila em memória
2395
+ * reintroduz exatamente a perda que este desenho existe para evitar.
2396
+ * 2. **Retentativa com backoff**: o destino dos hooks (n8n, IA, API de terceiro) também cai em
2397
+ * deploy. Sem retry, o job falha uma vez e a conversa morre do mesmo jeito.
2398
+ *
2399
+ * `jobId` é estável e derivado da mensagem: reentrega da Meta e re-enfileiramento produzem o mesmo
2400
+ * id, e a fila descarta o segundo em vez de rodar o efeito duas vezes.
2401
+ */
2402
+ interface InboundDispatchQueueInterface {
2403
+ enqueue(job: InboundDispatchJob, options: {
2404
+ jobId: string;
2405
+ }): Promise<void>;
2406
+ }
2407
+ declare function buildInboundJobId(job: InboundDispatchJob): string;
2408
+ declare function toSessionContract(row: SessionRow): ConversationSession;
2409
+ /**
2410
+ * Os efeitos de host de uma entrega já persistida. Vive fora do `ReceiveWebhookUseCase` porque
2411
+ * roda nos dois lados: inline, quando o host não configurou fila, e dentro do worker, quando
2412
+ * configurou. Um só corpo de regra para os dois caminhos — duas cópias divergiriam.
2413
+ */
2414
+ declare class InboundEffectsDispatcher {
2415
+ private readonly params;
2416
+ constructor(params: {
2417
+ sessionRepository: SessionRepository;
2418
+ hooks?: MetaWhatsAppHooks;
2419
+ realtime?: RealtimeNotifierInterface;
2420
+ });
2421
+ run(job: InboundDispatchJob): Promise<void>;
2422
+ runMessageEffects(job: InboundMessageEffectsJob): Promise<void>;
2423
+ runStatusEffects(job: InboundStatusEffectsJob): Promise<void>;
2424
+ }
2425
+ /**
2426
+ * Ponto de entrada do worker do host: recebe o job que a fila devolveu e roda os efeitos.
2427
+ *
2428
+ * Deixe a exceção propagar. É ela que faz a fila contar a tentativa e reagendar com backoff;
2429
+ * capturar aqui para logar transforma falha recuperável em mensagem perdida em silêncio.
2430
+ */
2431
+ declare class ProcessInboundDispatchUseCase {
2432
+ private readonly dispatcher;
2433
+ constructor(dispatcher: InboundEffectsDispatcher);
2434
+ execute(job: InboundDispatchJob): Promise<void>;
2435
+ }
1264
2436
 
1265
2437
  type ReceiveWebhookParams = {
1266
2438
  companyId: string;
@@ -1271,11 +2443,13 @@ type ReceiveWebhookResult = {
1271
2443
  duplicate: boolean;
1272
2444
  messagesProcessed: number;
1273
2445
  statusesProcessed: number;
2446
+ ignoredForeignNumber: number;
1274
2447
  };
1275
2448
  declare class ReceiveWebhookUseCase {
1276
2449
  private readonly params;
1277
2450
  constructor(params: {
1278
2451
  appSecret: string;
2452
+ phoneNumberId: string;
1279
2453
  nonceStore: NonceStoreInterface;
1280
2454
  sessionRepository: SessionRepository;
1281
2455
  messageRepository: MessageRepository;
@@ -1283,12 +2457,52 @@ declare class ReceiveWebhookUseCase {
1283
2457
  startState: SessionState;
1284
2458
  hooks?: MetaWhatsAppHooks;
1285
2459
  realtime?: RealtimeNotifierInterface;
2460
+ /**
2461
+ * Sem fila o módulo se comporta como sempre: os hooks rodam dentro da requisição do webhook.
2462
+ * Configurá-la é o que faz a conversa sobreviver a um deploy no meio do atendimento.
2463
+ */
2464
+ inboundQueue?: InboundDispatchQueueInterface;
1286
2465
  });
2466
+ private readonly dispatcher;
1287
2467
  execute(input: ReceiveWebhookParams): Promise<ReceiveWebhookResult>;
1288
2468
  private handleMessage;
1289
2469
  private handleStatus;
2470
+ private dispatch;
1290
2471
  }
1291
2472
 
2473
+ /**
2474
+ * Política efetiva de transcrição de uma empresa.
2475
+ *
2476
+ * A separação que este arquivo existe para manter: **ambiente decide se é POSSÍVEL, settings decide
2477
+ * se é para FAZER.** A capacidade (engine, chave, storage que sabe reler) é injetada pelo host e é
2478
+ * por deploy — chave de API não vai para tabela de configuração de tenant. Já "transcrever ou não" e
2479
+ * "automático ou sob demanda" são decisão de operação de cada empresa, e pedir deploy para mudar
2480
+ * isso é o que transforma um interruptor em ticket.
2481
+ */
2482
+ type TranscriptionPolicy = {
2483
+ readonly isEnabled: boolean;
2484
+ readonly mode: TranscriptionMode;
2485
+ };
2486
+ type TranscriptionPolicyDefaults = {
2487
+ /** O que vale quando o painel não decidiu. Tipicamente vem do ambiente do host. */
2488
+ readonly isEnabled: boolean;
2489
+ readonly mode: TranscriptionMode;
2490
+ };
2491
+ type ResolveTranscriptionPolicyDependencies = {
2492
+ readonly settingsRepository: SettingsRepository;
2493
+ readonly defaults: TranscriptionPolicyDefaults;
2494
+ };
2495
+ /**
2496
+ * Resolve a política por empresa, com o padrão do host como base.
2497
+ *
2498
+ * Uma consulta a `settings` por áudio transcrito. Não é cacheado de propósito: é uma leitura por
2499
+ * chave primária, acontece uma vez por nota de voz (não por mensagem), e cachear introduziria a
2500
+ * pergunta "por quanto tempo o operador continua vendo o interruptor antigo depois de mexer nele" —
2501
+ * custo real, para economizar um índice único.
2502
+ */
2503
+ declare function createTranscriptionPolicyResolver(dependencies: ResolveTranscriptionPolicyDependencies): (companyId: string) => Promise<TranscriptionPolicy>;
2504
+ type TranscriptionPolicyResolver = ReturnType<typeof createTranscriptionPolicyResolver>;
2505
+
1292
2506
  type IngestInboundMediaParams = {
1293
2507
  companyId: string;
1294
2508
  messageId: string;
@@ -1299,13 +2513,52 @@ type IngestInboundMediaParams = {
1299
2513
  type IngestInboundMediaResult = {
1300
2514
  uploadId: string;
1301
2515
  alreadyIngested: boolean;
2516
+ /**
2517
+ * Só presente quando a transcrição automática rodou nesta execução. Ausente é o normal: mídia que
2518
+ * não é áudio, transcrição desligada, modo sob demanda, ou mídia já ingerida antes.
2519
+ */
2520
+ transcription?: {
2521
+ status: TranscriptionStatus;
2522
+ };
2523
+ };
2524
+ /**
2525
+ * Transcrição durante a ingestão — o modo `auto`.
2526
+ *
2527
+ * Entra aqui, e não em use-case separado, por um motivo só: neste ponto o buffer do áudio ACABOU de
2528
+ * ser baixado e está em memória. Transcrever fora daqui custaria um segundo download do storage por
2529
+ * áudio, e o `TranscribeAudioUseCase` existe justamente para esse caso (sob demanda e retomada).
2530
+ */
2531
+ type IngestTranscriptionOptions = {
2532
+ transcriber: AudioTranscriber;
2533
+ messageRepository: MessageRepository;
2534
+ /**
2535
+ * Política POR EMPRESA, resolvida a cada áudio. Não é um `mode` fixo porque o interruptor mora nas
2536
+ * configurações da empresa: um valor capturado na construção do use-case congelaria a escolha até
2537
+ * o próximo deploy, e o worker é um processo longo — o operador mexeria no painel e nada mudaria.
2538
+ */
2539
+ resolvePolicy: TranscriptionPolicyResolver;
2540
+ languageHint?: string;
2541
+ hooks?: Pick<MetaWhatsAppHooks, 'onTranscriptionDeferred'>;
1302
2542
  };
1303
2543
  declare class IngestInboundMediaUseCase {
1304
2544
  private readonly db;
1305
2545
  private readonly channel;
1306
2546
  private readonly objectStorage;
1307
- constructor(db: MetaWhatsAppDatabase, channel: ChannelAdapterInterface, objectStorage: ObjectStorageInterface);
2547
+ private readonly documentRepository?;
2548
+ private readonly transcription?;
2549
+ constructor(db: MetaWhatsAppDatabase, channel: ChannelAdapterInterface, objectStorage: ObjectStorageInterface, documentRepository?: DocumentRepository | undefined, transcription?: IngestTranscriptionOptions | undefined);
1308
2550
  execute(params: IngestInboundMediaParams): Promise<IngestInboundMediaResult>;
2551
+ /**
2552
+ * Transcreve o áudio recém-baixado, quando o modo é `auto`.
2553
+ *
2554
+ * **Nunca propaga erro.** Neste ponto o binário já está no storage e já entrou na biblioteca da
2555
+ * conversa: deixar uma falha de transcrição subir marcaria a ingestão inteira como falha, e o
2556
+ * retry do host baixaria de novo da Meta um arquivo que está salvo — gastando banda para reproduzir
2557
+ * um efeito que já aconteceu. O status fica gravado na mensagem e o `onTranscriptionDeferred`
2558
+ * avisa quem sabe reenfileirar.
2559
+ */
2560
+ private transcribeIfAuto;
2561
+ private recordTranscriptionFailure;
1309
2562
  }
1310
2563
  declare function extractMediaDescriptor(message: MessageRow): {
1311
2564
  sourceMediaId: string;
@@ -1313,6 +2566,105 @@ declare function extractMediaDescriptor(message: MessageRow): {
1313
2566
  filename?: string;
1314
2567
  } | undefined;
1315
2568
 
2569
+ /**
2570
+ * Storage com leitura garantida. `getObject` é opcional no contrato, então quem monta este use-case
2571
+ * precisa provar que o método existe — sem os bytes não há o que transcrever, e um use-case que
2572
+ * sempre falha é pior do que a ausência ser visível no tipo.
2573
+ */
2574
+ type ReadableObjectStorage = ObjectStorageInterface & {
2575
+ getObject: NonNullable<ObjectStorageInterface['getObject']>;
2576
+ };
2577
+ type TranscribeAudioParams = {
2578
+ companyId: string;
2579
+ messageId: string;
2580
+ /**
2581
+ * Refaz mesmo com transcrição já salva. Serve ao "transcrever de novo" depois de trocar de engine
2582
+ * — sem isto, um resultado ruim do engine antigo ficaria congelado para sempre.
2583
+ */
2584
+ force?: boolean;
2585
+ };
2586
+ type TranscribeAudioResult = {
2587
+ status: TranscriptionStatus;
2588
+ text: string | null;
2589
+ language: string | null;
2590
+ engine: string | null;
2591
+ /** `true` quando devolveu o que já estava salvo, sem gastar cota. */
2592
+ alreadyTranscribed: boolean;
2593
+ };
2594
+ type TranscribeAudioDependencies = {
2595
+ messageRepository: MessageRepository;
2596
+ objectStorage: ReadableObjectStorage;
2597
+ transcriber: AudioTranscriber;
2598
+ /**
2599
+ * Política por empresa. Ausente, a transcrição sob demanda não consulta configuração nenhuma e
2600
+ * atende sempre — que é o comportamento de quem controla o liga/desliga só por ambiente.
2601
+ */
2602
+ resolvePolicy?: TranscriptionPolicyResolver;
2603
+ /** ISO 639-1 do produto. Informar corta a detecção do Whisper e evita pt-BR curto virar espanhol. */
2604
+ languageHint?: string;
2605
+ hooks?: Pick<MetaWhatsAppHooks, 'onTranscriptionDeferred'>;
2606
+ };
2607
+ /**
2608
+ * Transcreve o áudio de UMA mensagem já persistida e ingerida.
2609
+ *
2610
+ * Serve aos dois modos: é o que o painel chama no botão "transcrever" (`onDemand`) e é o que o host
2611
+ * chama ao retomar um `pending` reenfileirado. O modo `auto` não passa por aqui — ele transcreve
2612
+ * dentro da ingestão, onde o buffer já está em memória e não custa um segundo download.
2613
+ *
2614
+ * Idempotente por `transcription_status`: chamar de novo num `'done'` devolve o que está salvo em
2615
+ * vez de gastar cota transcrevendo o mesmo áudio.
2616
+ */
2617
+ declare class TranscribeAudioUseCase {
2618
+ private readonly dependencies;
2619
+ constructor(dependencies: TranscribeAudioDependencies);
2620
+ execute(params: TranscribeAudioParams): Promise<TranscribeAudioResult>;
2621
+ private transcribeBuffer;
2622
+ /**
2623
+ * Carimba o motivo antes de propagar. O status é o que impede os dois desperdícios simétricos:
2624
+ * reprocessar para sempre um codec impossível, e desistir de um áudio que só esbarrou na cota.
2625
+ */
2626
+ private persistFailure;
2627
+ }
2628
+ declare function resolveFailureStatus(error: unknown): TranscriptionStatus;
2629
+
2630
+ /**
2631
+ * Guarda um arquivo gravado no simulador e devolve o id que o webhook vai referenciar.
2632
+ *
2633
+ * Existe como use-case para o host só precisar da ROTA: receber o corpo, chamar isto, devolver o
2634
+ * `mediaId`. A parte que erra — onde gravar, com que chave, como marcar o id para o adaptador
2635
+ * reconhecer depois — fica aqui, num lugar só, e não em cada produto.
2636
+ *
2637
+ * O SDK para exatamente na porta do HTTP: registrar endpoint é do host, e um pacote que abrisse rota
2638
+ * no servidor de quem o instala decidiria caminho, autenticação e versionamento no lugar dele.
2639
+ */
2640
+ type StorePreviewMediaParams = {
2641
+ companyId: string;
2642
+ /** Bytes do arquivo. Quem converte de base64 é a rota — o use-case não conhece transporte. */
2643
+ buffer: Buffer;
2644
+ mimeType: string;
2645
+ filename?: string;
2646
+ };
2647
+ type StorePreviewMediaResult = {
2648
+ /** Já com o prefixo: é isto que o simulador manda no webhook. */
2649
+ mediaId: string;
2650
+ uploadId: string;
2651
+ };
2652
+ declare class StorePreviewMediaUseCase {
2653
+ private readonly objectStorage;
2654
+ /**
2655
+ * Fonte do sufixo único da chave. Injetada porque o módulo não escolhe gerador de id — e porque
2656
+ * um teste precisa de chave previsível.
2657
+ */
2658
+ private readonly generateKeySuffix;
2659
+ constructor(objectStorage: ObjectStorageInterface,
2660
+ /**
2661
+ * Fonte do sufixo único da chave. Injetada porque o módulo não escolhe gerador de id — e porque
2662
+ * um teste precisa de chave previsível.
2663
+ */
2664
+ generateKeySuffix: () => string);
2665
+ execute(params: StorePreviewMediaParams): Promise<StorePreviewMediaResult>;
2666
+ }
2667
+
1316
2668
  interface MetaWhatsAppModuleConfig {
1317
2669
  phoneNumberId: string;
1318
2670
  accessToken: string;
@@ -1321,15 +2673,66 @@ interface MetaWhatsAppModuleConfig {
1321
2673
  wabaId?: string;
1322
2674
  apiVersion?: string;
1323
2675
  baseUrl?: string;
2676
+ /**
2677
+ * Catálogo do Meta Commerce que a vitrine no chat oferece. Sem ele — ou sem `providers.catalog`
2678
+ * — a action `send_product_list` não é registrada: nó que o editor oferece e que em silêncio não
2679
+ * faz nada é pior do que nó que não existe.
2680
+ */
2681
+ catalogId?: string;
1324
2682
  }
1325
2683
  interface MetaWhatsAppModuleFeatures {
1326
2684
  flowEngine?: boolean;
2685
+ flowGraphCache?: boolean | {
2686
+ ttlSeconds?: number;
2687
+ };
2688
+ /**
2689
+ * Aceita mídia do simulador de conversa — o que faz o microfone aparecer no preview do cliente.
2690
+ *
2691
+ * **Desligado por omissão, e a decisão é consciente.** Ligado, o canal passa a aceitar id que não
2692
+ * veio da Meta: `preview-upload:<chave>` faz o servidor ler aquele objeto do storage. Em ambiente
2693
+ * de simulação isso é o recurso; em produção é leitura arbitrária do bucket por webhook forjado.
2694
+ *
2695
+ * Exige `providers.objectStorage` com `getObject` — sem os bytes não há o que devolver, e o flag é
2696
+ * ignorado em vez de produzir um canal que falha na primeira nota de voz.
2697
+ */
2698
+ previewMedia?: boolean;
2699
+ }
2700
+ /**
2701
+ * Transcrição de áudio. Ausente = desligada, e as colunas ficam nulas ("não avaliado").
2702
+ *
2703
+ * `mode` é a decisão de produto que este objeto existe para carregar: `auto` transcreve toda nota de
2704
+ * voz recebida, na ingestão, onde o buffer já está em memória; `onDemand` só transcreve quando o
2705
+ * atendente pede, gastando cota apenas com áudio que alguém vai ler de fato.
2706
+ */
2707
+ interface MetaWhatsAppTranscriptionConfig {
2708
+ transcriber: AudioTranscriber;
2709
+ /**
2710
+ * PADRÃO, não decisão final: vale para as empresas que não mexeram no interruptor do painel. As
2711
+ * configurações por empresa (`settings.transcriptionMode`) têm precedência.
2712
+ *
2713
+ * Padrão `onDemand` — o modo que não gasta cota sem alguém pedir.
2714
+ */
2715
+ mode?: TranscriptionMode;
2716
+ /**
2717
+ * PADRÃO de ligado/desligado para empresas sem escolha registrada. `true` — injetar o transcritor
2718
+ * já é a declaração de que o host quer o recurso; quem controla por empresa usa o painel.
2719
+ */
2720
+ isEnabledByDefault?: boolean;
2721
+ /** ISO 639-1 do produto (ex.: `'pt'`). Corta a detecção de idioma do engine. */
2722
+ languageHint?: string;
1327
2723
  }
1328
2724
  interface MetaWhatsAppModuleProviders {
1329
2725
  objectStorage?: ObjectStorageInterface;
2726
+ cache?: CacheInterface;
1330
2727
  realtime?: RealtimeNotifierInterface;
1331
2728
  subjectResolver?: SubjectResolverInterface;
1332
2729
  catalog?: CatalogPort;
2730
+ moderator?: MessageModerator;
2731
+ /**
2732
+ * Converte nota de voz em texto. Exige `objectStorage` com `getObject` para o modo sob demanda —
2733
+ * transcrever um áudio já salvo significa ler os bytes de volta.
2734
+ */
2735
+ transcription?: MetaWhatsAppTranscriptionConfig;
1333
2736
  }
1334
2737
  interface CreateMetaWhatsAppModuleParams {
1335
2738
  db: MetaWhatsAppDatabase;
@@ -1350,10 +2753,31 @@ declare function createMetaWhatsAppModule(params: CreateMetaWhatsAppModuleParams
1350
2753
  release: ReleaseConversationUseCase;
1351
2754
  list: ListConversationsUseCase;
1352
2755
  listMessages: ListMessagesUseCase;
2756
+ listDocuments: ListConversationDocumentsUseCase;
2757
+ listCompanyDocuments: ListCompanyDocumentsUseCase;
2758
+ delete: DeleteConversationUseCase;
2759
+ purgeExpiredDocuments: PurgeExpiredDocumentsUseCase;
1353
2760
  export: ExportConversationUseCase;
2761
+ transcribeAudio: TranscribeAudioUseCase | undefined;
1354
2762
  repository: SessionRepository;
2763
+ messageRepository: MessageRepository;
2764
+ documentRepository: DocumentRepository;
1355
2765
  };
2766
+ /**
2767
+ * `undefined` = o host não injetou transcritor, e nenhuma configuração de empresa muda isso: a
2768
+ * capacidade não existe. Presente, `resolvePolicy` responde o que vale para uma empresa —
2769
+ * é o que a rota de configurações usa para dizer ao painel se desenha o interruptor.
2770
+ */
2771
+ transcription: {
2772
+ defaultMode: TranscriptionMode;
2773
+ resolvePolicy: (companyId: string) => Promise<TranscriptionPolicy>;
2774
+ } | undefined;
1356
2775
  settings: SettingsRepository;
2776
+ /**
2777
+ * `undefined` quando o recurso não está ligado (ou falta storage legível). O host consulta a
2778
+ * ausência para não registrar a rota de upload — e o preview, sem a rota, esconde o microfone.
2779
+ */
2780
+ previewMedia: StorePreviewMediaUseCase | undefined;
1357
2781
  webhook: {
1358
2782
  receive: ReceiveWebhookUseCase;
1359
2783
  verifyChallenge: (query: {
@@ -1372,14 +2796,42 @@ declare function createMetaWhatsAppModule(params: CreateMetaWhatsAppModuleParams
1372
2796
  delete: DeleteFlowGraphUseCase;
1373
2797
  livePositions: GetLiveFlowPositionsUseCase;
1374
2798
  repository: FlowGraphRepository;
2799
+ mediaRepository: FlowMediaRepository;
1375
2800
  } | undefined;
1376
2801
  catalog: CatalogPort | undefined;
1377
2802
  };
1378
2803
  type MetaWhatsAppModule = ReturnType<typeof createMetaWhatsAppModule>;
1379
2804
 
2805
+ /**
2806
+ * Leitura de mídia do simulador de conversa.
2807
+ *
2808
+ * **Atrás de flag de propósito, e não ligado por omissão.** Aceitar id que não veio da Meta é
2809
+ * exatamente o que um webhook forjado exploraria: bastaria mandar `preview-upload:<chave>` para
2810
+ * fazer o servidor ler um objeto arbitrário do storage e devolvê-lo. Num ambiente de simulação isso
2811
+ * é o recurso; em produção é leitura arbitrária. Quem liga assume, e a decisão fica visível no
2812
+ * lugar onde o módulo é montado.
2813
+ */
2814
+ type PreviewMediaSupport = {
2815
+ readonly isEnabled: boolean;
2816
+ readonly objectStorage: ObjectStorageInterface & {
2817
+ getObject: NonNullable<ObjectStorageInterface['getObject']>;
2818
+ };
2819
+ /** Mime a devolver, já que o storage guarda bytes e não o tipo. Padrão `audio/ogg`. */
2820
+ readonly defaultMimeType?: string;
2821
+ };
1380
2822
  declare class WhatsAppChannelAdapter implements ChannelAdapterInterface {
1381
2823
  private readonly messages;
1382
- constructor(messages: WhatsAppMessageProvider);
2824
+ /**
2825
+ * Ausente, o adaptador se comporta como sempre: todo id vai para a Graph API. É o que garante
2826
+ * que atualizar o pacote não abre nada em quem não pediu.
2827
+ */
2828
+ private readonly previewMedia?;
2829
+ constructor(messages: WhatsAppMessageProvider,
2830
+ /**
2831
+ * Ausente, o adaptador se comporta como sempre: todo id vai para a Graph API. É o que garante
2832
+ * que atualizar o pacote não abre nada em quem não pediu.
2833
+ */
2834
+ previewMedia?: PreviewMediaSupport | undefined);
1383
2835
  private translateErrors;
1384
2836
  sendText(to: string, body: string): Promise<{
1385
2837
  externalMessageId: string | null;
@@ -1412,6 +2864,34 @@ declare class WhatsAppChannelAdapter implements ChannelAdapterInterface {
1412
2864
  }): Promise<{
1413
2865
  externalMessageId: string | null;
1414
2866
  }>;
2867
+ sendInteractiveButtons(params: {
2868
+ to: string;
2869
+ body: string;
2870
+ buttons: {
2871
+ id: string;
2872
+ title: string;
2873
+ }[];
2874
+ }): Promise<{
2875
+ externalMessageId: string | null;
2876
+ }>;
2877
+ sendProductList(params: {
2878
+ to: string;
2879
+ headerText: string;
2880
+ body: string;
2881
+ footerText?: string;
2882
+ sections: {
2883
+ title: string;
2884
+ retailerIds: string[];
2885
+ }[];
2886
+ }): Promise<{
2887
+ externalMessageId: string | null;
2888
+ }>;
2889
+ /**
2890
+ * Busca o binário da mídia — da Meta, ou do storage quando o id é do simulador.
2891
+ *
2892
+ * O desvio acontece ANTES de qualquer chamada de rede: id do simulador não existe na Meta, e
2893
+ * tentar buscá-lo lá renderia um 404 confuso em vez do áudio que o operador acabou de gravar.
2894
+ */
1415
2895
  fetchMediaAsBase64(mediaId: string): Promise<{
1416
2896
  data: string;
1417
2897
  mimeType: string;
@@ -1455,4 +2935,70 @@ declare function redeemSseTicket(store: TicketStoreInterface, ticket: string): P
1455
2935
  whatsappNumber: string;
1456
2936
  } | null>;
1457
2937
 
1458
- export { type CreateFlowGraphParams, CreateFlowGraphUseCase, type CreateMetaWhatsAppModuleParams, DeleteFlowGraphUseCase, type DrizzleMigrateFunction, type ExportConversationParams, type ExportConversationResult, ExportConversationUseCase, FlowGraphRepository, type FlowGraphRow, FlowInterpreter, type FlowRunResult, type FlowStepInput, type FlowStepResult, GetFlowGraphUseCase, GetLiveFlowPositionsUseCase, type IngestInboundMediaParams, type IngestInboundMediaResult, IngestInboundMediaUseCase, type InsertMessageParams, InvalidFlowGraphError, type ListConversationsFilters, type ListConversationsParams, ListConversationsUseCase, ListFlowGraphsUseCase, type ListMessagesParams, ListMessagesUseCase, type LogMessageParams, LogMessageUseCase, META_WHATSAPP_MIGRATIONS_TABLE, MessageRepository, type MessageRow, type MetaWhatsAppDatabase, type MetaWhatsAppModule, type MetaWhatsAppModuleConfig, type MetaWhatsAppModuleFeatures, type MetaWhatsAppModuleProviders, type NewFlowGraphRow, type NewMessageRow, type NewSessionRow, type NewSettingsRow, type NonceStoreInterface, OptimisticLockError, type RealtimeRelay, type ReceiveWebhookParams, type ReceiveWebhookResult, ReceiveWebhookUseCase, type ReleaseConversationParams, ReleaseConversationUseCase, type ListMessagesParams$1 as RepositoryListMessagesParams, type RunMetaWhatsAppMigrationsParams, type SaveFlowGraphParams, SaveFlowGraphUseCase, type SendMediaParams, SendMessageUseCase, type SendTemplateParams, type SendTextParams, SessionRepository, type SessionRow, SettingsRepository, type SettingsRow, SseHub, type SseListener, type TakeoverConversationParams, TakeoverConversationUseCase, type TicketStoreInterface, WEBHOOK_NONCE_TTL_SECONDS, WhatsAppChannelAdapter, claimWebhookDelivery, createMetaWhatsAppModule, extractMediaDescriptor, flowGraphs, issueSseTicket, messages, metaWhatsAppMigrationsFolder, metaWhatsAppSchema, redeemSseTicket, runMetaWhatsAppMigrations, sessions, settings, verifyWebhookChallenge, verifyWebhookSignature };
2938
+ /**
2939
+ * Porta de escrita no transcript, e não a classe `LogMessageUseCase`.
2940
+ *
2941
+ * Exigir a classe amarraria a action a quem já guarda as mensagens nas tabelas do módulo — um host
2942
+ * em migração, com o transcript ainda no schema dele, não conseguiria usar a única action built-in
2943
+ * sem gravar o envio numa tabela que o painel dele não lê. O que a action precisa é só de um lugar
2944
+ * para registrar o que saiu.
2945
+ */
2946
+ type FlowMediaTranscriptLogger = {
2947
+ execute(params: LogMessageParams): Promise<unknown>;
2948
+ };
2949
+ type CreateSendMediaActionParams = {
2950
+ flowMediaRepository: FlowMediaRepository;
2951
+ objectStorage: ObjectStorageInterface & {
2952
+ getObject: NonNullable<ObjectStorageInterface['getObject']>;
2953
+ };
2954
+ logMessage: FlowMediaTranscriptLogger;
2955
+ startState: SessionState;
2956
+ onError?: (error: unknown, details: {
2957
+ flowKey: string;
2958
+ nodeId: string;
2959
+ uploadId: string;
2960
+ }) => void;
2961
+ };
2962
+ /**
2963
+ * Action `send_media`: ao passar pelo nó, envia os arquivos que a biblioteca tem anexados a ele.
2964
+ *
2965
+ * É a única action built-in que o módulo implementa de ponta a ponta — as outras
2966
+ * (`handoff`, `send_product_list`) dependem de regra de negócio do produto. Esta não: "mandar
2967
+ * estes arquivos ao chegar aqui" é comportamento de canal, e replicá-la em cada host seria
2968
+ * copiar o mesmo código com os mesmos bugs.
2969
+ *
2970
+ * O nó não guarda os arquivos em `actionParams` de propósito. Se guardasse, trocar o material
2971
+ * exigiria editar e republicar o grafo — e o ponto todo é o contrário: quem cuida do conteúdo
2972
+ * troca o arquivo na biblioteca e o fluxo continua igual.
2973
+ */
2974
+ declare function createSendMediaAction(params: CreateSendMediaActionParams): FlowActionHandler;
2975
+
2976
+ /** Tetos da Meta para `interactive.product_list`. Acima disso a mensagem inteira é recusada. */
2977
+ declare const PRODUCT_LIST_LIMIT: {
2978
+ readonly ITEMS: 30;
2979
+ readonly SECTIONS: 10;
2980
+ };
2981
+ type CreateSendProductListActionParams = {
2982
+ catalog: CatalogPort;
2983
+ catalogId: string;
2984
+ logMessage: FlowMediaTranscriptLogger;
2985
+ startState: SessionState;
2986
+ /** Falha de UMA vitrine não pode travar a conversa; o host observa por aqui. */
2987
+ onError?: (error: unknown, details: {
2988
+ flowKey: string;
2989
+ nodeId: string;
2990
+ }) => void;
2991
+ };
2992
+ /**
2993
+ * Action `send_product_list`: ao passar pelo nó, envia a vitrine do catálogo publicado na Meta.
2994
+ *
2995
+ * O nó guarda **critério**, não a lista de produtos. Congelar `retailerId` em `actionParams` faria
2996
+ * o fluxo continuar oferecendo o item esgotado — ou o item excluído, que a Meta recusa junto com a
2997
+ * mensagem inteira. O que o operador edita é o texto e o filtro; o estoque manda no resto.
2998
+ *
2999
+ * Só produto em estoque entra. Vitrine que mostra o que não tem produz a pior conversa possível:
3000
+ * o cliente escolhe, responde, e recebe "acabou".
3001
+ */
3002
+ declare function createSendProductListAction(params: CreateSendProductListActionParams): FlowActionHandler;
3003
+
3004
+ export { type AttachFlowMediaParams, type AudioTranscriber, type CompanyDocumentView, type CompanyDocumentsPage, type ConversationDocumentView, type ConversationDocumentsPage, type CreateFlowGraphParams, CreateFlowGraphUseCase, type CreateMetaWhatsAppModuleParams, type CreateSendMediaActionParams, type CreateSendProductListActionParams, DEFAULT_FLOW_GRAPH_CACHE_TTL_SECONDS, type DeleteConversationParams, type DeleteConversationResult, DeleteConversationUseCase, DeleteFlowGraphUseCase, DocumentRepository, type DocumentRow, type DrizzleMigrateFunction, type ExportConversationParams, type ExportConversationResult, ExportConversationUseCase, FlowGraphCache, FlowGraphRepository, type FlowGraphRow, FlowInterpreter, type FlowMediaLocation, FlowMediaRepository, type FlowMediaRow, type FlowMediaTranscriptLogger, type FlowRunResult, type FlowStepInput, type FlowStepResult, GetFlowGraphUseCase, GetLiveFlowPositionsUseCase, type InboundDispatchJob, type InboundDispatchQueueInterface, InboundEffectsDispatcher, type InboundMediaDescriptor, type InboundMessageEffectsJob, type InboundStatusEffectsJob, type IngestInboundMediaParams, type IngestInboundMediaResult, IngestInboundMediaUseCase, type IngestTranscriptionOptions, type InsertMessageParams, InvalidFlowGraphError, type LinkDocumentParams, type ListCompanyDocumentsParams, ListCompanyDocumentsUseCase, type ListConversationDocumentsParams, ListConversationDocumentsUseCase, type ListConversationsFilters, type ListConversationsParams, ListConversationsUseCase, type ListDocumentsParams, type ListDocumentsResult, ListFlowGraphsUseCase, type ListMessagesParams, ListMessagesUseCase, type LogMessageParams, LogMessageUseCase, META_WHATSAPP_MIGRATIONS_TABLE, type MessageModerator, MessageRepository, type MessageRow, type MetaWhatsAppDatabase, type MetaWhatsAppModule, type MetaWhatsAppModuleConfig, type MetaWhatsAppModuleFeatures, type MetaWhatsAppModuleProviders, type MetaWhatsAppTranscriptionConfig, type NewDocumentRow, type NewFlowGraphRow, type NewFlowMediaRow, type NewMessageRow, type NewSessionRow, type NewSettingsRow, type NonceStoreInterface, OptimisticLockError, PRODUCT_LIST_LIMIT, type PreviewMediaSupport, ProcessInboundDispatchUseCase, type PurgeExpiredDocumentsParams, type PurgeExpiredDocumentsResult, PurgeExpiredDocumentsUseCase, type RealtimeRelay, type ReceiveWebhookParams, type ReceiveWebhookResult, ReceiveWebhookUseCase, type ReleaseConversationParams, ReleaseConversationUseCase, type ListMessagesParams$1 as RepositoryListMessagesParams, type ResolveTranscriptionPolicyDependencies, type RunMetaWhatsAppMigrationsParams, type SaveFlowGraphParams, SaveFlowGraphUseCase, type SaveTranscriptionByWaMessageIdParams, type SaveTranscriptionParams, type SendMediaParams, SendMessageUseCase, type SendTemplateParams, type SendTextParams, SessionRepository, type SessionRow, SettingsRepository, type SettingsRow, SseHub, type SseListener, type StorePreviewMediaParams, type StorePreviewMediaResult, StorePreviewMediaUseCase, TRANSCRIPTION_MODE, TRANSCRIPTION_STATUS, type TakeoverConversationParams, TakeoverConversationUseCase, type TicketStoreInterface, type TranscribeAudioDependencies, type TranscribeAudioParams, type TranscribeAudioResult, TranscribeAudioUseCase, type TranscriptionPolicy, type TranscriptionPolicyDefaults, type TranscriptionPolicyResolver, type TranscriptionStatus, type UpdateFlowMediaParams, WhatsAppChannelAdapter, buildInboundJobId, claimWebhookDelivery, confirmWebhookDelivery, createMetaWhatsAppModule, createSendMediaAction, createSendProductListAction, createTranscriptionPolicyResolver, documents, extractMediaDescriptor, flowGraphs, flowMedia, isAudioMimeType, isRetriableTranscriptionError, isUnsupportedTranscriptionError, issueSseTicket, messages, metaWhatsAppMigrationsFolder, metaWhatsAppSchema, redeemSseTicket, resolveFailureStatus, runMetaWhatsAppMigrations, sessions, settings, toSessionContract, transcriptionRetryAfterSeconds, verifyWebhookChallenge, verifyWebhookSignature };