@adatechnology/meta-whatsapp-module 0.1.0

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.cjs ADDED
@@ -0,0 +1,1560 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc2) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc2 = __getOwnPropDesc(from, key)) || desc2.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ CreateFlowGraphUseCase: () => CreateFlowGraphUseCase,
24
+ DeleteFlowGraphUseCase: () => DeleteFlowGraphUseCase,
25
+ ExportConversationUseCase: () => ExportConversationUseCase,
26
+ FlowGraphRepository: () => FlowGraphRepository,
27
+ FlowInterpreter: () => FlowInterpreter,
28
+ GetFlowGraphUseCase: () => GetFlowGraphUseCase,
29
+ GetLiveFlowPositionsUseCase: () => GetLiveFlowPositionsUseCase,
30
+ IngestInboundMediaUseCase: () => IngestInboundMediaUseCase,
31
+ InvalidFlowGraphError: () => InvalidFlowGraphError,
32
+ ListConversationsUseCase: () => ListConversationsUseCase,
33
+ ListFlowGraphsUseCase: () => ListFlowGraphsUseCase,
34
+ ListMessagesUseCase: () => ListMessagesUseCase,
35
+ LogMessageUseCase: () => LogMessageUseCase,
36
+ MessageRepository: () => MessageRepository,
37
+ OptimisticLockError: () => OptimisticLockError,
38
+ ReceiveWebhookUseCase: () => ReceiveWebhookUseCase,
39
+ ReleaseConversationUseCase: () => ReleaseConversationUseCase,
40
+ SaveFlowGraphUseCase: () => SaveFlowGraphUseCase,
41
+ SendMessageUseCase: () => SendMessageUseCase,
42
+ SessionRepository: () => SessionRepository,
43
+ SettingsRepository: () => SettingsRepository,
44
+ SseHub: () => SseHub,
45
+ TakeoverConversationUseCase: () => TakeoverConversationUseCase,
46
+ WEBHOOK_NONCE_TTL_SECONDS: () => WEBHOOK_NONCE_TTL_SECONDS,
47
+ WhatsAppChannelAdapter: () => WhatsAppChannelAdapter,
48
+ claimWebhookDelivery: () => claimWebhookDelivery,
49
+ createMetaWhatsAppModule: () => createMetaWhatsAppModule,
50
+ extractMediaDescriptor: () => extractMediaDescriptor,
51
+ flowGraphs: () => flowGraphs,
52
+ issueSseTicket: () => issueSseTicket,
53
+ messages: () => messages,
54
+ metaWhatsAppSchema: () => metaWhatsAppSchema,
55
+ redeemSseTicket: () => redeemSseTicket,
56
+ runMetaWhatsAppMigrations: () => runMetaWhatsAppMigrations,
57
+ sessions: () => sessions,
58
+ settings: () => settings,
59
+ verifyWebhookChallenge: () => verifyWebhookChallenge,
60
+ verifyWebhookSignature: () => verifyWebhookSignature
61
+ });
62
+ module.exports = __toCommonJS(index_exports);
63
+
64
+ // src/createMetaWhatsAppModule.ts
65
+ var import_meta_whatsapp_provider = require("@adatechnology/meta-whatsapp-provider");
66
+
67
+ // src/repositories/SessionRepository.ts
68
+ var import_drizzle_orm2 = require("drizzle-orm");
69
+
70
+ // src/schema/schema.ts
71
+ var import_drizzle_orm = require("drizzle-orm");
72
+ var import_pg_core = require("drizzle-orm/pg-core");
73
+ var metaWhatsAppSchema = (0, import_pg_core.pgSchema)("meta_whatsapp");
74
+ var sessions = metaWhatsAppSchema.table("sessions", {
75
+ id: (0, import_pg_core.uuid)("id").primaryKey().defaultRandom(),
76
+ // Multiempresa: toda entidade carrega companyId, vindo do contexto autenticado do host —
77
+ // nunca de um campo livre do cliente (ver database.md "Consistência e multiempresa").
78
+ companyId: (0, import_pg_core.uuid)("company_id").notNull(),
79
+ whatsappNumber: (0, import_pg_core.varchar)("whatsapp_number", {
80
+ length: 20
81
+ }).notNull(),
82
+ // varchar (não enum): a máquina de estados de cada produto evolui de forma independente do módulo.
83
+ currentState: (0, import_pg_core.varchar)("current_state", {
84
+ length: 64
85
+ }).notNull().default("start"),
86
+ // Posição no grafo, quando o motor de fluxo está em uso — colunas separadas em vez de
87
+ // convencionar "flowKey:nodeId" dentro de currentState: além de ser um contrato implícito que
88
+ // nada garantia, a concatenação estourava o varchar(64) (flow key já é varchar(64) sozinha).
89
+ flowKey: (0, import_pg_core.varchar)("flow_key", {
90
+ length: 64
91
+ }),
92
+ currentNodeId: (0, import_pg_core.varchar)("current_node_id", {
93
+ length: 64
94
+ }),
95
+ context: (0, import_pg_core.jsonb)("context").$type().notNull().default({}),
96
+ mode: (0, import_pg_core.varchar)("mode", {
97
+ length: 12
98
+ }).notNull().default("bot"),
99
+ assignedUserId: (0, import_pg_core.uuid)("assigned_user_id"),
100
+ humanRequestedAt: (0, import_pg_core.timestamp)("human_requested_at", {
101
+ withTimezone: true
102
+ }),
103
+ lastInboundAt: (0, import_pg_core.timestamp)("last_inbound_at", {
104
+ withTimezone: true
105
+ }),
106
+ lastAgentReadAt: (0, import_pg_core.timestamp)("last_agent_read_at", {
107
+ withTimezone: true
108
+ }),
109
+ lastActivity: (0, import_pg_core.timestamp)("last_activity", {
110
+ withTimezone: true
111
+ }).notNull().defaultNow(),
112
+ createdAt: (0, import_pg_core.timestamp)("created_at", {
113
+ withTimezone: true
114
+ }).notNull().defaultNow(),
115
+ updatedAt: (0, import_pg_core.timestamp)("updated_at", {
116
+ withTimezone: true
117
+ }).notNull().defaultNow()
118
+ }, (table) => [
119
+ // Único por empresa (não globalmente) — dois hosts diferentes podem atender o mesmo número.
120
+ (0, import_pg_core.uniqueIndex)("idx_sessions_company_number").on(table.companyId, table.whatsappNumber),
121
+ (0, import_pg_core.index)("idx_sessions_company_mode").on(table.companyId, table.mode),
122
+ // Alimenta getLiveFlowPositions (agregação por nó do grafo).
123
+ (0, import_pg_core.index)("idx_sessions_company_flow_node").on(table.companyId, table.flowKey, table.currentNodeId)
124
+ ]);
125
+ var messages = metaWhatsAppSchema.table("messages", {
126
+ id: (0, import_pg_core.uuid)("id").primaryKey().defaultRandom(),
127
+ companyId: (0, import_pg_core.uuid)("company_id").notNull(),
128
+ sessionId: (0, import_pg_core.uuid)("session_id").notNull().references(() => sessions.id, {
129
+ onDelete: "cascade"
130
+ }),
131
+ whatsappNumber: (0, import_pg_core.varchar)("whatsapp_number", {
132
+ length: 20
133
+ }).notNull(),
134
+ direction: (0, import_pg_core.varchar)("direction", {
135
+ length: 12
136
+ }).notNull(),
137
+ sender: (0, import_pg_core.varchar)("sender", {
138
+ length: 12
139
+ }).notNull(),
140
+ agentUserId: (0, import_pg_core.uuid)("agent_user_id"),
141
+ type: (0, import_pg_core.varchar)("type", {
142
+ length: 16
143
+ }).notNull().default("text"),
144
+ content: (0, import_pg_core.text)("content"),
145
+ // T5.4 — nunca base64 aqui; mídia vive no ObjectStorageInterface do host, referenciada por
146
+ // uploadId dentro deste jsonb.
147
+ payload: (0, import_pg_core.jsonb)("payload").$type(),
148
+ waMessageId: (0, import_pg_core.varchar)("wa_message_id", {
149
+ length: 128
150
+ }),
151
+ status: (0, import_pg_core.varchar)("status", {
152
+ length: 16
153
+ }),
154
+ readAt: (0, import_pg_core.timestamp)("read_at", {
155
+ withTimezone: true
156
+ }),
157
+ createdAt: (0, import_pg_core.timestamp)("created_at", {
158
+ withTimezone: true
159
+ }).notNull().defaultNow()
160
+ }, (table) => [
161
+ (0, import_pg_core.index)("idx_messages_session_created").on(table.sessionId, table.createdAt),
162
+ (0, import_pg_core.index)("idx_messages_company_number_created").on(table.companyId, table.whatsappNumber, table.createdAt),
163
+ // Parcial e ÚNICO: é o que de fato garante idempotência de entrega. Um SELECT-antes-de-INSERT
164
+ // não basta — a Meta reenvia a mesma entrega e várias instâncias do host processam em
165
+ // paralelo, então as duas passariam pela checagem e inseririam duplicado. Parcial porque
166
+ // mensagens outbound ainda sem waMessageId (envio em curso) são legitimamente NULL, e NULLs
167
+ // não podem competir entre si por unicidade.
168
+ (0, import_pg_core.uniqueIndex)("idx_messages_company_wa_message_id").on(table.companyId, table.waMessageId).where(import_drizzle_orm.sql`${table.waMessageId} is not null`)
169
+ ]);
170
+ var flowGraphs = metaWhatsAppSchema.table("flow_graphs", {
171
+ id: (0, import_pg_core.uuid)("id").primaryKey().defaultRandom(),
172
+ companyId: (0, import_pg_core.uuid)("company_id").notNull(),
173
+ key: (0, import_pg_core.varchar)("key", {
174
+ length: 64
175
+ }).notNull(),
176
+ label: (0, import_pg_core.varchar)("label", {
177
+ length: 120
178
+ }).notNull(),
179
+ startNodeId: (0, import_pg_core.varchar)("start_node_id", {
180
+ length: 64
181
+ }).notNull(),
182
+ nodes: (0, import_pg_core.jsonb)("nodes").$type().notNull().default({}),
183
+ // Otimista: o editor manda a versão que carregou; salvar com versão desatualizada é
184
+ // rejeitado pelo host (evita duas abas sobrescreverem uma a outra silenciosamente).
185
+ version: (0, import_pg_core.integer)("version").notNull().default(1),
186
+ showInMenu: (0, import_pg_core.boolean)("show_in_menu").notNull().default(false),
187
+ menuOptionLabel: (0, import_pg_core.varchar)("menu_option_label", {
188
+ length: 64
189
+ }),
190
+ createdAt: (0, import_pg_core.timestamp)("created_at", {
191
+ withTimezone: true
192
+ }).notNull().defaultNow(),
193
+ updatedAt: (0, import_pg_core.timestamp)("updated_at", {
194
+ withTimezone: true
195
+ }).notNull().defaultNow()
196
+ }, (table) => [
197
+ (0, import_pg_core.uniqueIndex)("idx_flow_graphs_company_key").on(table.companyId, table.key)
198
+ ]);
199
+ var settings = metaWhatsAppSchema.table("settings", {
200
+ companyId: (0, import_pg_core.uuid)("company_id").primaryKey(),
201
+ templateName: (0, import_pg_core.varchar)("template_name", {
202
+ length: 128
203
+ }),
204
+ templateLanguage: (0, import_pg_core.varchar)("template_language", {
205
+ length: 16
206
+ }).notNull().default("pt_BR"),
207
+ // Mapa posicional {{1}}, {{2}}... → token resolvido no envio (ex.: '{clientName}').
208
+ templateVariables: (0, import_pg_core.jsonb)("template_variables").$type().notNull().default([]),
209
+ welcomeMessage: (0, import_pg_core.text)("welcome_message"),
210
+ farewellMessage: (0, import_pg_core.text)("farewell_message"),
211
+ createdAt: (0, import_pg_core.timestamp)("created_at", {
212
+ withTimezone: true
213
+ }).notNull().defaultNow(),
214
+ updatedAt: (0, import_pg_core.timestamp)("updated_at", {
215
+ withTimezone: true
216
+ }).notNull().defaultNow()
217
+ });
218
+
219
+ // src/repositories/SessionRepository.ts
220
+ var DEFAULT_LIMIT = 20;
221
+ var SessionRepository = class {
222
+ static {
223
+ __name(this, "SessionRepository");
224
+ }
225
+ db;
226
+ constructor(db) {
227
+ this.db = db;
228
+ }
229
+ async getContext(companyId, whatsappNumber) {
230
+ const [row] = await this.db.select().from(sessions).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(sessions.companyId, companyId), (0, import_drizzle_orm2.eq)(sessions.whatsappNumber, whatsappNumber))).limit(1);
231
+ return row;
232
+ }
233
+ async getOrCreate(companyId, whatsappNumber, startState) {
234
+ const existing = await this.getContext(companyId, whatsappNumber);
235
+ if (existing) return existing;
236
+ const [created] = await this.db.insert(sessions).values({
237
+ companyId,
238
+ whatsappNumber,
239
+ currentState: startState
240
+ }).onConflictDoUpdate({
241
+ target: [
242
+ sessions.companyId,
243
+ sessions.whatsappNumber
244
+ ],
245
+ set: {
246
+ lastActivity: import_drizzle_orm2.sql`now()`
247
+ }
248
+ }).returning();
249
+ return created;
250
+ }
251
+ async setState(companyId, whatsappNumber, state, context) {
252
+ await this.db.update(sessions).set({
253
+ currentState: state,
254
+ ...context !== void 0 ? {
255
+ context
256
+ } : {},
257
+ lastActivity: import_drizzle_orm2.sql`now()`,
258
+ updatedAt: import_drizzle_orm2.sql`now()`
259
+ }).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(sessions.companyId, companyId), (0, import_drizzle_orm2.eq)(sessions.whatsappNumber, whatsappNumber)));
260
+ }
261
+ // Posição no grafo de fluxo — chamado pelo host a cada transição do FlowInterpreter.
262
+ // Passar null em ambos desliga o rastreio (ex.: conversa saiu do motor de fluxo).
263
+ async setFlowPosition(companyId, whatsappNumber, flowKey, currentNodeId) {
264
+ await this.db.update(sessions).set({
265
+ flowKey,
266
+ currentNodeId,
267
+ lastActivity: import_drizzle_orm2.sql`now()`,
268
+ updatedAt: import_drizzle_orm2.sql`now()`
269
+ }).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(sessions.companyId, companyId), (0, import_drizzle_orm2.eq)(sessions.whatsappNumber, whatsappNumber)));
270
+ }
271
+ // Marca a chegada de uma mensagem DO CLIENTE. É este carimbo — e não lastActivity, que também
272
+ // se move em ações do atendente/bot — que define a janela de 24h do WhatsApp: fora dela, só
273
+ // template aprovado reabre a conversa (ver WindowExpiredError nos contracts).
274
+ async touchInbound(companyId, whatsappNumber) {
275
+ await this.db.update(sessions).set({
276
+ lastInboundAt: import_drizzle_orm2.sql`now()`,
277
+ lastActivity: import_drizzle_orm2.sql`now()`,
278
+ updatedAt: import_drizzle_orm2.sql`now()`
279
+ }).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(sessions.companyId, companyId), (0, import_drizzle_orm2.eq)(sessions.whatsappNumber, whatsappNumber)));
280
+ }
281
+ // Horas desde a última mensagem do cliente — undefined se ele nunca escreveu. O canal (Fase 5)
282
+ // usa isto para decidir entre mensagem livre e template antes de chamar a Graph API.
283
+ async hoursSinceLastInbound(companyId, whatsappNumber) {
284
+ const session = await this.getContext(companyId, whatsappNumber);
285
+ if (!session?.lastInboundAt) return void 0;
286
+ return (Date.now() - session.lastInboundAt.getTime()) / (1e3 * 60 * 60);
287
+ }
288
+ async setMode(companyId, whatsappNumber, mode, assignedUserId) {
289
+ await this.db.update(sessions).set({
290
+ mode,
291
+ assignedUserId: assignedUserId ?? null,
292
+ updatedAt: import_drizzle_orm2.sql`now()`
293
+ }).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(sessions.companyId, companyId), (0, import_drizzle_orm2.eq)(sessions.whatsappNumber, whatsappNumber)));
294
+ }
295
+ // T3.3 — takeover: atendente assume a conversa, tirando-a do modo bot.
296
+ async takeover(companyId, whatsappNumber, agentUserId) {
297
+ await this.setMode(companyId, whatsappNumber, "human", agentUserId);
298
+ }
299
+ // T3.3 — release: devolve a conversa ao bot.
300
+ async release(companyId, whatsappNumber) {
301
+ await this.setMode(companyId, whatsappNumber, "bot", null);
302
+ }
303
+ async requestHuman(companyId, whatsappNumber) {
304
+ await this.db.update(sessions).set({
305
+ humanRequestedAt: import_drizzle_orm2.sql`now()`,
306
+ updatedAt: import_drizzle_orm2.sql`now()`
307
+ }).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(sessions.companyId, companyId), (0, import_drizzle_orm2.eq)(sessions.whatsappNumber, whatsappNumber)));
308
+ }
309
+ async markRead(companyId, whatsappNumber) {
310
+ await this.db.update(sessions).set({
311
+ lastAgentReadAt: import_drizzle_orm2.sql`now()`
312
+ }).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(sessions.companyId, companyId), (0, import_drizzle_orm2.eq)(sessions.whatsappNumber, whatsappNumber)));
313
+ }
314
+ async markAllRead(companyId, userId) {
315
+ const result = await this.db.update(sessions).set({
316
+ lastAgentReadAt: import_drizzle_orm2.sql`now()`
317
+ }).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(sessions.companyId, companyId), (0, import_drizzle_orm2.eq)(sessions.assignedUserId, userId)));
318
+ return Number(result.rowCount ?? 0);
319
+ }
320
+ async listByContextFilters(companyId, filters) {
321
+ const limit = filters.limit ?? DEFAULT_LIMIT;
322
+ const offset = ((filters.page ?? 1) - 1) * limit;
323
+ const conditions = [
324
+ (0, import_drizzle_orm2.eq)(sessions.companyId, companyId)
325
+ ];
326
+ if (filters.waitingHuman) conditions.push(import_drizzle_orm2.sql`${sessions.humanRequestedAt} is not null`);
327
+ if (filters.search) conditions.push(import_drizzle_orm2.sql`${sessions.whatsappNumber} ilike ${"%" + filters.search + "%"}`);
328
+ const rows = await this.db.select({
329
+ id: sessions.id,
330
+ whatsappNumber: sessions.whatsappNumber,
331
+ mode: sessions.mode,
332
+ assignedUserId: sessions.assignedUserId,
333
+ currentState: sessions.currentState,
334
+ lastActivity: sessions.lastActivity,
335
+ lastInboundAt: sessions.lastInboundAt,
336
+ humanRequestedAt: sessions.humanRequestedAt
337
+ }).from(sessions).where((0, import_drizzle_orm2.and)(...conditions)).orderBy((0, import_drizzle_orm2.desc)(sessions.lastActivity)).limit(limit).offset(offset);
338
+ return rows.map((row) => ({
339
+ id: row.id,
340
+ whatsappNumber: row.whatsappNumber,
341
+ lastAt: row.lastActivity.toISOString(),
342
+ lastInboundAt: row.lastInboundAt?.toISOString() ?? null,
343
+ mode: row.mode,
344
+ assignedUserId: row.assignedUserId,
345
+ waitingHuman: row.humanRequestedAt !== null,
346
+ unread: 0,
347
+ currentState: row.currentState
348
+ }));
349
+ }
350
+ // T3.3 — export: transcript completo de uma conversa, mensagens em ordem cronológica.
351
+ async exportConversation(companyId, whatsappNumber) {
352
+ const session = await this.getContext(companyId, whatsappNumber);
353
+ if (!session) return null;
354
+ const rows = await this.db.select().from(messages).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(messages.companyId, companyId), (0, import_drizzle_orm2.eq)(messages.sessionId, session.id))).orderBy(messages.createdAt);
355
+ return {
356
+ session,
357
+ messages: rows
358
+ };
359
+ }
360
+ };
361
+
362
+ // src/repositories/MessageRepository.ts
363
+ var import_drizzle_orm3 = require("drizzle-orm");
364
+ var DEFAULT_LIMIT2 = 50;
365
+ var MessageRepository = class {
366
+ static {
367
+ __name(this, "MessageRepository");
368
+ }
369
+ db;
370
+ constructor(db) {
371
+ this.db = db;
372
+ }
373
+ // Idempotente por (companyId, waMessageId) — o mesmo webhook chega mais de uma vez (a Meta
374
+ // reenvia em instabilidade) e várias instâncias do host processam em paralelo. A garantia é o
375
+ // índice único parcial no banco + onConflictDoNothing, NÃO um SELECT prévio: duas entregas
376
+ // concorrentes passariam as duas pela checagem e inseririam duplicado.
377
+ // Devolve undefined quando a mensagem já existia (nada foi inserido).
378
+ async insertMessage(params) {
379
+ const values = {
380
+ companyId: params.companyId,
381
+ sessionId: params.sessionId,
382
+ whatsappNumber: params.whatsappNumber,
383
+ direction: params.direction,
384
+ sender: params.sender,
385
+ agentUserId: params.agentUserId ?? null,
386
+ type: params.type,
387
+ content: params.content ?? null,
388
+ payload: params.payload ?? null,
389
+ waMessageId: params.waMessageId ?? null,
390
+ status: params.status ?? null
391
+ };
392
+ const [created] = await this.db.insert(messages).values(values).onConflictDoNothing().returning();
393
+ return created;
394
+ }
395
+ async updateMessageStatus(companyId, waMessageId, status) {
396
+ const [updated] = await this.db.update(messages).set({
397
+ status,
398
+ ...status === "read" ? {
399
+ readAt: /* @__PURE__ */ new Date()
400
+ } : {}
401
+ }).where((0, import_drizzle_orm3.and)((0, import_drizzle_orm3.eq)(messages.companyId, companyId), (0, import_drizzle_orm3.eq)(messages.waMessageId, waMessageId))).returning();
402
+ return updated;
403
+ }
404
+ async listByConversation(params) {
405
+ const conditions = [
406
+ (0, import_drizzle_orm3.eq)(messages.companyId, params.companyId),
407
+ (0, import_drizzle_orm3.eq)(messages.sessionId, params.sessionId)
408
+ ];
409
+ if (params.before) conditions.push((0, import_drizzle_orm3.lt)(messages.createdAt, new Date(params.before)));
410
+ return this.db.select().from(messages).where((0, import_drizzle_orm3.and)(...conditions)).orderBy(messages.createdAt).limit(params.limit ?? DEFAULT_LIMIT2);
411
+ }
412
+ };
413
+
414
+ // src/repositories/FlowGraphRepository.ts
415
+ var import_drizzle_orm4 = require("drizzle-orm");
416
+ var import_meta_whatsapp_contracts = require("@adatechnology/meta-whatsapp-contracts");
417
+ var InvalidFlowGraphError = class extends Error {
418
+ static {
419
+ __name(this, "InvalidFlowGraphError");
420
+ }
421
+ validationMessage;
422
+ constructor(key, validationMessage) {
423
+ super(`Grafo do fluxo "${key}" est\xE1 malformado no banco: ${validationMessage}`), this.validationMessage = validationMessage;
424
+ this.name = "InvalidFlowGraphError";
425
+ }
426
+ };
427
+ function toContractGraph(row) {
428
+ const parsed = import_meta_whatsapp_contracts.flowGraphNodesSchema.safeParse(row.nodes);
429
+ if (!parsed.success) throw new InvalidFlowGraphError(row.key, parsed.error.message);
430
+ return {
431
+ key: row.key,
432
+ label: row.label,
433
+ startNodeId: row.startNodeId,
434
+ version: row.version,
435
+ nodes: parsed.data
436
+ };
437
+ }
438
+ __name(toContractGraph, "toContractGraph");
439
+ var OptimisticLockError = class extends Error {
440
+ static {
441
+ __name(this, "OptimisticLockError");
442
+ }
443
+ constructor(key) {
444
+ super(`Fluxo "${key}" foi alterado por outra sess\xE3o \u2014 recarregue antes de salvar.`);
445
+ this.name = "OptimisticLockError";
446
+ }
447
+ };
448
+ var FlowGraphRepository = class {
449
+ static {
450
+ __name(this, "FlowGraphRepository");
451
+ }
452
+ db;
453
+ constructor(db) {
454
+ this.db = db;
455
+ }
456
+ async get(companyId, key) {
457
+ const [row] = await this.db.select().from(flowGraphs).where((0, import_drizzle_orm4.and)((0, import_drizzle_orm4.eq)(flowGraphs.companyId, companyId), (0, import_drizzle_orm4.eq)(flowGraphs.key, key))).limit(1);
458
+ return row ? toContractGraph(row) : void 0;
459
+ }
460
+ async list(companyId) {
461
+ const rows = await this.db.select().from(flowGraphs).where((0, import_drizzle_orm4.eq)(flowGraphs.companyId, companyId));
462
+ return rows.map((row) => ({
463
+ key: row.key,
464
+ label: row.label,
465
+ nodeCount: Object.keys(row.nodes).length,
466
+ showInMenu: row.showInMenu,
467
+ menuOptionLabel: row.menuOptionLabel ?? void 0,
468
+ updatedAt: row.updatedAt.toISOString()
469
+ }));
470
+ }
471
+ // Valida antes de gravar: aceitar um grafo malformado e só reclamar na leitura empurraria a
472
+ // falha para o meio de uma conversa real, em vez de barrar quem salvou.
473
+ assertValidNodes(key, nodes) {
474
+ const parsed = import_meta_whatsapp_contracts.flowGraphNodesSchema.safeParse(nodes);
475
+ if (!parsed.success) throw new InvalidFlowGraphError(key, parsed.error.message);
476
+ }
477
+ async create(companyId, graph) {
478
+ this.assertValidNodes(graph.key, graph.nodes);
479
+ const [created] = await this.db.insert(flowGraphs).values({
480
+ companyId,
481
+ key: graph.key,
482
+ label: graph.label,
483
+ startNodeId: graph.startNodeId,
484
+ nodes: graph.nodes,
485
+ showInMenu: graph.showInMenu ?? false,
486
+ menuOptionLabel: graph.menuOptionLabel
487
+ }).returning();
488
+ return toContractGraph(created);
489
+ }
490
+ // Lock otimista: a escrita só aplica se `expectedVersion` ainda bater com o que está salvo —
491
+ // senão, alguém mais salvou entretanto e o editor precisa recarregar (ver comentário no schema).
492
+ async save(companyId, graph, expectedVersion) {
493
+ this.assertValidNodes(graph.key, graph.nodes);
494
+ const rows = await this.db.update(flowGraphs).set({
495
+ label: graph.label,
496
+ startNodeId: graph.startNodeId,
497
+ nodes: graph.nodes,
498
+ version: expectedVersion + 1,
499
+ updatedAt: /* @__PURE__ */ new Date()
500
+ }).where((0, import_drizzle_orm4.and)((0, import_drizzle_orm4.eq)(flowGraphs.companyId, companyId), (0, import_drizzle_orm4.eq)(flowGraphs.key, graph.key), (0, import_drizzle_orm4.eq)(flowGraphs.version, expectedVersion))).returning();
501
+ if (rows.length === 0) throw new OptimisticLockError(graph.key);
502
+ return toContractGraph(rows[0]);
503
+ }
504
+ async delete(companyId, key) {
505
+ await this.db.delete(flowGraphs).where((0, import_drizzle_orm4.and)((0, import_drizzle_orm4.eq)(flowGraphs.companyId, companyId), (0, import_drizzle_orm4.eq)(flowGraphs.key, key)));
506
+ }
507
+ // T4.2 — GetLiveFlowPositions: agrega sessões ativas por (flowKey, currentNodeId), lendo as
508
+ // colunas dedicadas gravadas por SessionRepository.setFlowPosition. Agrega no banco (GROUP BY,
509
+ // coberto por idx_sessions_company_flow_node) em vez de trazer toda sessão da empresa para
510
+ // contar em memória.
511
+ async getLiveFlowPositions(companyId) {
512
+ const rows = await this.db.select({
513
+ flowKey: sessions.flowKey,
514
+ nodeId: sessions.currentNodeId,
515
+ count: import_drizzle_orm4.sql`count(*)::int`
516
+ }).from(sessions).where((0, import_drizzle_orm4.and)((0, import_drizzle_orm4.eq)(sessions.companyId, companyId), (0, import_drizzle_orm4.eq)(sessions.mode, "bot"), (0, import_drizzle_orm4.isNotNull)(sessions.flowKey), (0, import_drizzle_orm4.isNotNull)(sessions.currentNodeId))).groupBy(sessions.flowKey, sessions.currentNodeId);
517
+ return rows.map((row) => ({
518
+ flowKey: row.flowKey,
519
+ nodeId: row.nodeId,
520
+ count: row.count
521
+ }));
522
+ }
523
+ };
524
+
525
+ // src/repositories/SettingsRepository.ts
526
+ var import_drizzle_orm5 = require("drizzle-orm");
527
+ function toContractSettings(row) {
528
+ return {
529
+ templateName: row.templateName ?? "",
530
+ templateLanguage: row.templateLanguage,
531
+ templateVariables: row.templateVariables,
532
+ welcomeMessage: row.welcomeMessage ?? "",
533
+ farewellMessage: row.farewellMessage ?? ""
534
+ };
535
+ }
536
+ __name(toContractSettings, "toContractSettings");
537
+ var EMPTY_SETTINGS = {
538
+ templateName: "",
539
+ templateLanguage: "pt_BR",
540
+ templateVariables: [],
541
+ welcomeMessage: "",
542
+ farewellMessage: ""
543
+ };
544
+ var SettingsRepository = class {
545
+ static {
546
+ __name(this, "SettingsRepository");
547
+ }
548
+ db;
549
+ constructor(db) {
550
+ this.db = db;
551
+ }
552
+ // Empresa sem linha de configuração é o estado normal logo após instalar o módulo — devolve
553
+ // os defaults em vez de undefined, para o chamador não precisar tratar "ainda não configurado"
554
+ // como caso especial em todo lugar.
555
+ async get(companyId) {
556
+ const [row] = await this.db.select().from(settings).where((0, import_drizzle_orm5.eq)(settings.companyId, companyId)).limit(1);
557
+ return row ? toContractSettings(row) : EMPTY_SETTINGS;
558
+ }
559
+ async save(companyId, update) {
560
+ const current = await this.get(companyId);
561
+ const merged = {
562
+ ...current,
563
+ ...update
564
+ };
565
+ const [row] = await this.db.insert(settings).values({
566
+ companyId,
567
+ templateName: merged.templateName || null,
568
+ templateLanguage: merged.templateLanguage,
569
+ templateVariables: merged.templateVariables,
570
+ welcomeMessage: merged.welcomeMessage || null,
571
+ farewellMessage: merged.farewellMessage || null
572
+ }).onConflictDoUpdate({
573
+ target: settings.companyId,
574
+ set: {
575
+ templateName: merged.templateName || null,
576
+ templateLanguage: merged.templateLanguage,
577
+ templateVariables: merged.templateVariables,
578
+ welcomeMessage: merged.welcomeMessage || null,
579
+ farewellMessage: merged.farewellMessage || null,
580
+ updatedAt: /* @__PURE__ */ new Date()
581
+ }
582
+ }).returning();
583
+ return toContractSettings(row);
584
+ }
585
+ // Resolve {{1}}, {{2}}... a partir do mapa configurado + o contexto da conversa. Ex.: se
586
+ // templateVariables = ['{clientName}', '{city}'], devolve [nome, cidade] na ordem posicional
587
+ // que a Graph API espera nos bodyParameters.
588
+ async resolveTemplateVariables(companyId, context) {
589
+ const { templateVariables } = await this.get(companyId);
590
+ return templateVariables.map((token) => {
591
+ const key = token.replace(/^\{|\}$/g, "");
592
+ const value = context[key];
593
+ return value === void 0 || value === null ? "" : String(value);
594
+ });
595
+ }
596
+ };
597
+
598
+ // src/use-cases/LogMessage.use-case.ts
599
+ var LogMessageUseCase = class {
600
+ static {
601
+ __name(this, "LogMessageUseCase");
602
+ }
603
+ sessionRepository;
604
+ messageRepository;
605
+ realtime;
606
+ constructor(sessionRepository, messageRepository, realtime) {
607
+ this.sessionRepository = sessionRepository;
608
+ this.messageRepository = messageRepository;
609
+ this.realtime = realtime;
610
+ }
611
+ async execute(params) {
612
+ const session = await this.sessionRepository.getOrCreate(params.companyId, params.whatsappNumber, params.startState);
613
+ const saved = await this.messageRepository.insertMessage({
614
+ ...params,
615
+ sessionId: session.id
616
+ });
617
+ if (!saved) return void 0;
618
+ if (params.direction === "inbound") {
619
+ await this.sessionRepository.touchInbound(params.companyId, params.whatsappNumber);
620
+ }
621
+ this.realtime?.emit(`conv:${params.whatsappNumber}`, "message", {
622
+ direction: params.direction,
623
+ sender: params.sender
624
+ });
625
+ this.realtime?.emit("global", "data-changed", {});
626
+ return saved;
627
+ }
628
+ };
629
+
630
+ // src/use-cases/SendMessage.use-case.ts
631
+ var import_meta_whatsapp_contracts2 = require("@adatechnology/meta-whatsapp-contracts");
632
+ var WHATSAPP_WINDOW_HOURS = 24;
633
+ var SendMessageUseCase = class {
634
+ static {
635
+ __name(this, "SendMessageUseCase");
636
+ }
637
+ channel;
638
+ sessionRepository;
639
+ logMessage;
640
+ objectStorage;
641
+ constructor(channel, sessionRepository, logMessage, objectStorage) {
642
+ this.channel = channel;
643
+ this.sessionRepository = sessionRepository;
644
+ this.logMessage = logMessage;
645
+ this.objectStorage = objectStorage;
646
+ }
647
+ async assertWithinWindow(companyId, whatsappNumber) {
648
+ const hours = await this.sessionRepository.hoursSinceLastInbound(companyId, whatsappNumber);
649
+ if (hours === void 0) throw new import_meta_whatsapp_contracts2.WindowExpiredError();
650
+ if (hours >= WHATSAPP_WINDOW_HOURS) throw new import_meta_whatsapp_contracts2.WindowExpiredError(hours);
651
+ }
652
+ async sendText(params) {
653
+ await this.assertWithinWindow(params.companyId, params.whatsappNumber);
654
+ const { externalMessageId } = await this.channel.sendText(params.whatsappNumber, params.body);
655
+ return this.logMessage.execute({
656
+ companyId: params.companyId,
657
+ whatsappNumber: params.whatsappNumber,
658
+ direction: "outbound",
659
+ sender: params.sender,
660
+ agentUserId: params.agentUserId ?? null,
661
+ type: "text",
662
+ content: params.body,
663
+ waMessageId: externalMessageId,
664
+ status: "sent",
665
+ startState: params.startState
666
+ });
667
+ }
668
+ async sendMedia(params) {
669
+ await this.assertWithinWindow(params.companyId, params.whatsappNumber);
670
+ const { externalMessageId } = await this.channel.sendMedia({
671
+ to: params.whatsappNumber,
672
+ buffer: params.buffer,
673
+ mimeType: params.mimeType,
674
+ filename: params.filename,
675
+ caption: params.caption
676
+ });
677
+ const uploadId = this.objectStorage ? (await this.objectStorage.upload({
678
+ buffer: params.buffer,
679
+ mimeType: params.mimeType,
680
+ key: `meta-whatsapp/${params.companyId}/${Date.now()}-${params.filename}`
681
+ })).uploadId : void 0;
682
+ return this.logMessage.execute({
683
+ companyId: params.companyId,
684
+ whatsappNumber: params.whatsappNumber,
685
+ direction: "outbound",
686
+ sender: params.sender,
687
+ agentUserId: params.agentUserId ?? null,
688
+ type: mediaTypeFor(params.mimeType),
689
+ content: params.caption ?? params.filename,
690
+ payload: {
691
+ filename: params.filename,
692
+ mimeType: params.mimeType,
693
+ ...uploadId ? {
694
+ uploadId
695
+ } : {}
696
+ },
697
+ waMessageId: externalMessageId,
698
+ status: "sent",
699
+ startState: params.startState
700
+ });
701
+ }
702
+ // Template é o único envio que ignora a janela — é justamente o mecanismo que a Meta oferece
703
+ // para reabri-la.
704
+ async sendTemplate(params) {
705
+ const { externalMessageId } = await this.channel.sendTemplate({
706
+ to: params.whatsappNumber,
707
+ templateName: params.templateName,
708
+ languageCode: params.languageCode ?? "pt_BR",
709
+ bodyParameters: params.bodyParameters
710
+ });
711
+ return this.logMessage.execute({
712
+ companyId: params.companyId,
713
+ whatsappNumber: params.whatsappNumber,
714
+ direction: "outbound",
715
+ sender: params.sender,
716
+ agentUserId: params.agentUserId ?? null,
717
+ type: "template",
718
+ content: params.templateName,
719
+ payload: {
720
+ templateName: params.templateName,
721
+ bodyParameters: params.bodyParameters ?? []
722
+ },
723
+ waMessageId: externalMessageId,
724
+ status: "sent",
725
+ startState: params.startState
726
+ });
727
+ }
728
+ };
729
+ function mediaTypeFor(mimeType) {
730
+ if (mimeType.startsWith("image/")) return "image";
731
+ if (mimeType.startsWith("audio/")) return "audio";
732
+ if (mimeType.startsWith("video/")) return "video";
733
+ return "document";
734
+ }
735
+ __name(mediaTypeFor, "mediaTypeFor");
736
+
737
+ // src/use-cases/TakeoverConversation.use-case.ts
738
+ var TakeoverConversationUseCase = class {
739
+ static {
740
+ __name(this, "TakeoverConversationUseCase");
741
+ }
742
+ sessionRepository;
743
+ realtime;
744
+ constructor(sessionRepository, realtime) {
745
+ this.sessionRepository = sessionRepository;
746
+ this.realtime = realtime;
747
+ }
748
+ async execute(params) {
749
+ await this.sessionRepository.takeover(params.companyId, params.whatsappNumber, params.agentUserId);
750
+ this.realtime?.emit(`conv:${params.whatsappNumber}`, "mode-changed", {
751
+ mode: "human",
752
+ assignedUserId: params.agentUserId
753
+ });
754
+ this.realtime?.emit("global", "data-changed", {});
755
+ }
756
+ };
757
+
758
+ // src/use-cases/ReleaseConversation.use-case.ts
759
+ var ReleaseConversationUseCase = class {
760
+ static {
761
+ __name(this, "ReleaseConversationUseCase");
762
+ }
763
+ sessionRepository;
764
+ realtime;
765
+ constructor(sessionRepository, realtime) {
766
+ this.sessionRepository = sessionRepository;
767
+ this.realtime = realtime;
768
+ }
769
+ async execute(params) {
770
+ await this.sessionRepository.release(params.companyId, params.whatsappNumber);
771
+ this.realtime?.emit(`conv:${params.whatsappNumber}`, "mode-changed", {
772
+ mode: "bot",
773
+ assignedUserId: null
774
+ });
775
+ this.realtime?.emit("global", "data-changed", {});
776
+ }
777
+ };
778
+
779
+ // src/use-cases/ListConversations.use-case.ts
780
+ var ListConversationsUseCase = class {
781
+ static {
782
+ __name(this, "ListConversationsUseCase");
783
+ }
784
+ sessionRepository;
785
+ constructor(sessionRepository) {
786
+ this.sessionRepository = sessionRepository;
787
+ }
788
+ async execute(params) {
789
+ return this.sessionRepository.listByContextFilters(params.companyId, params.filters ?? {});
790
+ }
791
+ };
792
+
793
+ // src/use-cases/ListMessages.use-case.ts
794
+ var import_meta_whatsapp_contracts3 = require("@adatechnology/meta-whatsapp-contracts");
795
+ var ListMessagesUseCase = class {
796
+ static {
797
+ __name(this, "ListMessagesUseCase");
798
+ }
799
+ sessionRepository;
800
+ messageRepository;
801
+ constructor(sessionRepository, messageRepository) {
802
+ this.sessionRepository = sessionRepository;
803
+ this.messageRepository = messageRepository;
804
+ }
805
+ async execute(params) {
806
+ const session = await this.sessionRepository.getContext(params.companyId, params.whatsappNumber);
807
+ if (!session) throw new import_meta_whatsapp_contracts3.SessionNotFoundError(params.whatsappNumber);
808
+ const repoParams = {
809
+ companyId: params.companyId,
810
+ sessionId: session.id,
811
+ limit: params.limit,
812
+ before: params.before
813
+ };
814
+ return this.messageRepository.listByConversation(repoParams);
815
+ }
816
+ };
817
+
818
+ // src/use-cases/ExportConversation.use-case.ts
819
+ var import_meta_whatsapp_contracts4 = require("@adatechnology/meta-whatsapp-contracts");
820
+ var ExportConversationUseCase = class {
821
+ static {
822
+ __name(this, "ExportConversationUseCase");
823
+ }
824
+ sessionRepository;
825
+ constructor(sessionRepository) {
826
+ this.sessionRepository = sessionRepository;
827
+ }
828
+ async execute(params) {
829
+ const result = await this.sessionRepository.exportConversation(params.companyId, params.whatsappNumber);
830
+ if (!result) throw new import_meta_whatsapp_contracts4.SessionNotFoundError(params.whatsappNumber);
831
+ return result;
832
+ }
833
+ };
834
+
835
+ // src/use-cases/FlowGraph.use-cases.ts
836
+ var GetFlowGraphUseCase = class {
837
+ static {
838
+ __name(this, "GetFlowGraphUseCase");
839
+ }
840
+ flowGraphRepository;
841
+ constructor(flowGraphRepository) {
842
+ this.flowGraphRepository = flowGraphRepository;
843
+ }
844
+ async execute(params) {
845
+ return this.flowGraphRepository.get(params.companyId, params.key);
846
+ }
847
+ };
848
+ var ListFlowGraphsUseCase = class {
849
+ static {
850
+ __name(this, "ListFlowGraphsUseCase");
851
+ }
852
+ flowGraphRepository;
853
+ constructor(flowGraphRepository) {
854
+ this.flowGraphRepository = flowGraphRepository;
855
+ }
856
+ async execute(params) {
857
+ return this.flowGraphRepository.list(params.companyId);
858
+ }
859
+ };
860
+ var CreateFlowGraphUseCase = class {
861
+ static {
862
+ __name(this, "CreateFlowGraphUseCase");
863
+ }
864
+ flowGraphRepository;
865
+ constructor(flowGraphRepository) {
866
+ this.flowGraphRepository = flowGraphRepository;
867
+ }
868
+ async execute(params) {
869
+ return this.flowGraphRepository.create(params.companyId, params);
870
+ }
871
+ };
872
+ var SaveFlowGraphUseCase = class {
873
+ static {
874
+ __name(this, "SaveFlowGraphUseCase");
875
+ }
876
+ flowGraphRepository;
877
+ constructor(flowGraphRepository) {
878
+ this.flowGraphRepository = flowGraphRepository;
879
+ }
880
+ async execute(params) {
881
+ return this.flowGraphRepository.save(params.companyId, params.graph, params.expectedVersion);
882
+ }
883
+ };
884
+ var DeleteFlowGraphUseCase = class {
885
+ static {
886
+ __name(this, "DeleteFlowGraphUseCase");
887
+ }
888
+ flowGraphRepository;
889
+ constructor(flowGraphRepository) {
890
+ this.flowGraphRepository = flowGraphRepository;
891
+ }
892
+ async execute(params) {
893
+ return this.flowGraphRepository.delete(params.companyId, params.key);
894
+ }
895
+ };
896
+ var GetLiveFlowPositionsUseCase = class {
897
+ static {
898
+ __name(this, "GetLiveFlowPositionsUseCase");
899
+ }
900
+ flowGraphRepository;
901
+ constructor(flowGraphRepository) {
902
+ this.flowGraphRepository = flowGraphRepository;
903
+ }
904
+ async execute(params) {
905
+ return this.flowGraphRepository.getLiveFlowPositions(params.companyId);
906
+ }
907
+ };
908
+
909
+ // src/flows/FlowInterpreter.ts
910
+ var import_meta_whatsapp_contracts5 = require("@adatechnology/meta-whatsapp-contracts");
911
+ var DEFAULT_MAX_STEPS = 50;
912
+ function evaluateCondition(node, context) {
913
+ if (!node.conditionContextKey || !node.conditionOperator || !node.conditionValue) return false;
914
+ const actual = context[node.conditionContextKey];
915
+ const expected = node.conditionValue;
916
+ switch (node.conditionOperator) {
917
+ case "==":
918
+ return String(actual) === expected;
919
+ case "!=":
920
+ return String(actual) !== expected;
921
+ case "contains":
922
+ return typeof actual === "string" && actual.includes(expected);
923
+ case ">":
924
+ case ">=":
925
+ case "<":
926
+ case "<=": {
927
+ const actualNum = Number(actual);
928
+ const expectedNum = Number(expected);
929
+ if (Number.isNaN(actualNum) || Number.isNaN(expectedNum)) return false;
930
+ if (node.conditionOperator === ">") return actualNum > expectedNum;
931
+ if (node.conditionOperator === ">=") return actualNum >= expectedNum;
932
+ if (node.conditionOperator === "<") return actualNum < expectedNum;
933
+ return actualNum <= expectedNum;
934
+ }
935
+ default:
936
+ return false;
937
+ }
938
+ }
939
+ __name(evaluateCondition, "evaluateCondition");
940
+ function resolveNext(node, answerId) {
941
+ if (!node.next) return void 0;
942
+ if (typeof node.next === "string") return node.next;
943
+ if (answerId && node.next.byAnswer[answerId]) return node.next.byAnswer[answerId];
944
+ return node.next.default;
945
+ }
946
+ __name(resolveNext, "resolveNext");
947
+ var FlowInterpreter = class {
948
+ static {
949
+ __name(this, "FlowInterpreter");
950
+ }
951
+ actionHandlers = /* @__PURE__ */ new Map();
952
+ registerFlowAction(kind, handler) {
953
+ this.actionHandlers.set(kind, handler);
954
+ }
955
+ async step(input) {
956
+ const node = input.graph.nodes[input.currentNodeId];
957
+ if (!node) return {
958
+ kind: "terminal",
959
+ context: input.context
960
+ };
961
+ if (node.type === "condition") {
962
+ const branch = evaluateCondition(node, input.context) ? "true" : "false";
963
+ return this.moveTo(input, resolveNext(node, branch));
964
+ }
965
+ if (node.type === "action") {
966
+ const handler = node.actionKind ? this.actionHandlers.get(node.actionKind) : void 0;
967
+ if (!handler) return {
968
+ kind: "terminal",
969
+ context: input.context
970
+ };
971
+ const result = await handler({
972
+ node,
973
+ session: input.session,
974
+ channel: input.channel,
975
+ context: input.context
976
+ });
977
+ const contextAfterAction = result?.context ? {
978
+ ...input.context,
979
+ ...result.context
980
+ } : input.context;
981
+ const nextNodeId2 = result?.next ?? resolveNext(node, void 0);
982
+ return this.moveTo({
983
+ ...input,
984
+ context: contextAfterAction
985
+ }, nextNodeId2);
986
+ }
987
+ const isChoice = node.type === "menu" || node.questionType === "choice";
988
+ if (input.userAnswer === void 0) {
989
+ return {
990
+ kind: "awaiting-answer",
991
+ nodeId: input.currentNodeId,
992
+ context: input.context
993
+ };
994
+ }
995
+ const updatedContext = node.contextKey ? {
996
+ ...input.context,
997
+ [node.contextKey]: input.userAnswer
998
+ } : input.context;
999
+ const answerId = isChoice ? input.userAnswer : void 0;
1000
+ const nextNodeId = resolveNext(node, answerId);
1001
+ return this.moveTo({
1002
+ ...input,
1003
+ context: updatedContext
1004
+ }, nextNodeId);
1005
+ }
1006
+ moveTo(input, nextNodeId) {
1007
+ if (!nextNodeId) return {
1008
+ kind: "terminal",
1009
+ context: input.context
1010
+ };
1011
+ if ((0, import_meta_whatsapp_contracts5.isCrossFlowTarget)(nextNodeId)) return {
1012
+ kind: "cross-flow",
1013
+ flowKey: (0, import_meta_whatsapp_contracts5.crossFlowKey)(nextNodeId),
1014
+ context: input.context
1015
+ };
1016
+ if (!input.graph.nodes[nextNodeId]) return {
1017
+ kind: "terminal",
1018
+ context: input.context
1019
+ };
1020
+ return {
1021
+ kind: "advanced",
1022
+ nodeId: nextNodeId,
1023
+ context: input.context
1024
+ };
1025
+ }
1026
+ // Avança pelo grafo até parar num ponto que exige algo de fora: uma pergunta esperando resposta
1027
+ // do cliente, o fim do fluxo, ou um salto para outro fluxo. Nós de condição e ação não pedem
1028
+ // input nenhum, então parar neles obrigaria o chamador a reimplementar este laço — e sem
1029
+ // proteção, um grafo com ciclo de condições (A→B→A) o prenderia para sempre.
1030
+ //
1031
+ // O corte por `maxSteps` é uma rede de segurança de runtime, não a defesa principal: um ciclo é
1032
+ // erro de autoria e deveria ser barrado na publicação. Como validateGraph (no editor) hoje
1033
+ // detecta nó inalcançável mas NÃO detecta ciclo, um grafo cíclico é publicável — então o
1034
+ // runtime precisa sobreviver a ele em vez de travar o processo que atende o webhook.
1035
+ async run(input, options) {
1036
+ const maxSteps = options?.maxSteps ?? DEFAULT_MAX_STEPS;
1037
+ const visited = [
1038
+ input.currentNodeId
1039
+ ];
1040
+ let current = input;
1041
+ let steps = 0;
1042
+ while (steps < maxSteps) {
1043
+ const result = await this.step(current);
1044
+ steps++;
1045
+ if (result.kind !== "advanced") return {
1046
+ ...result,
1047
+ visited,
1048
+ steps
1049
+ };
1050
+ visited.push(result.nodeId);
1051
+ current = {
1052
+ ...current,
1053
+ currentNodeId: result.nodeId,
1054
+ context: result.context,
1055
+ userAnswer: void 0
1056
+ };
1057
+ }
1058
+ return {
1059
+ kind: "max-steps-exceeded",
1060
+ context: current.context,
1061
+ visited,
1062
+ steps
1063
+ };
1064
+ }
1065
+ };
1066
+
1067
+ // src/channel/WhatsAppChannelAdapter.ts
1068
+ var import_meta_graph_core = require("@adatechnology/meta-graph-core");
1069
+ var import_meta_whatsapp_contracts6 = require("@adatechnology/meta-whatsapp-contracts");
1070
+ var WhatsAppChannelAdapter = class {
1071
+ static {
1072
+ __name(this, "WhatsAppChannelAdapter");
1073
+ }
1074
+ messages;
1075
+ constructor(messages2) {
1076
+ this.messages = messages2;
1077
+ }
1078
+ async translateErrors(operation) {
1079
+ try {
1080
+ return await operation();
1081
+ } catch (error) {
1082
+ if (error instanceof import_meta_graph_core.WhatsAppWindowExpiredError) throw new import_meta_whatsapp_contracts6.WindowExpiredError();
1083
+ throw error;
1084
+ }
1085
+ }
1086
+ async sendText(to, body) {
1087
+ const result = await this.translateErrors(() => this.messages.sendText(to, body));
1088
+ return {
1089
+ externalMessageId: result.waMessageId
1090
+ };
1091
+ }
1092
+ async sendMedia(params) {
1093
+ const result = await this.translateErrors(() => this.messages.sendMedia(params));
1094
+ return {
1095
+ externalMessageId: result.waMessageId
1096
+ };
1097
+ }
1098
+ async sendTemplate(params) {
1099
+ const result = await this.translateErrors(() => this.messages.sendTemplate(params));
1100
+ return {
1101
+ externalMessageId: result.waMessageId
1102
+ };
1103
+ }
1104
+ async sendInteractiveList(params) {
1105
+ const result = await this.translateErrors(() => this.messages.sendInteractiveList({
1106
+ to: params.to,
1107
+ bodyText: params.body,
1108
+ buttonText: params.buttonLabel,
1109
+ sections: [
1110
+ {
1111
+ title: params.buttonLabel,
1112
+ rows: params.rows
1113
+ }
1114
+ ]
1115
+ }));
1116
+ return {
1117
+ externalMessageId: result.waMessageId
1118
+ };
1119
+ }
1120
+ async fetchMediaAsBase64(mediaId) {
1121
+ return this.translateErrors(() => this.messages.fetchMediaAsBase64(mediaId));
1122
+ }
1123
+ };
1124
+
1125
+ // src/channel/ReceiveWebhook.use-case.ts
1126
+ var import_meta_whatsapp_contracts8 = require("@adatechnology/meta-whatsapp-contracts");
1127
+
1128
+ // src/channel/webhookSecurity.ts
1129
+ var import_node_crypto = require("crypto");
1130
+ var import_meta_whatsapp_contracts7 = require("@adatechnology/meta-whatsapp-contracts");
1131
+ var WEBHOOK_NONCE_TTL_SECONDS = 300;
1132
+ function safeEqualStrings(left, right) {
1133
+ const leftDigest = (0, import_node_crypto.createHmac)("sha256", "constant-time-compare").update(left).digest();
1134
+ const rightDigest = (0, import_node_crypto.createHmac)("sha256", "constant-time-compare").update(right).digest();
1135
+ return (0, import_node_crypto.timingSafeEqual)(leftDigest, rightDigest);
1136
+ }
1137
+ __name(safeEqualStrings, "safeEqualStrings");
1138
+ function verifyWebhookChallenge(params) {
1139
+ if (params.mode !== "subscribe" || !params.token || !params.challenge) {
1140
+ throw new import_meta_whatsapp_contracts7.InvalidWebhookSignatureError();
1141
+ }
1142
+ if (!safeEqualStrings(params.token, params.expectedToken)) {
1143
+ throw new import_meta_whatsapp_contracts7.InvalidWebhookSignatureError();
1144
+ }
1145
+ return params.challenge;
1146
+ }
1147
+ __name(verifyWebhookChallenge, "verifyWebhookChallenge");
1148
+ function verifyWebhookSignature(params) {
1149
+ const { rawBody, signatureHeader, appSecret } = params;
1150
+ if (!signatureHeader?.startsWith("sha256=")) throw new import_meta_whatsapp_contracts7.InvalidWebhookSignatureError();
1151
+ const expected = (0, import_node_crypto.createHmac)("sha256", appSecret).update(rawBody).digest("hex");
1152
+ const received = signatureHeader.slice("sha256=".length);
1153
+ if (!safeEqualStrings(received, expected)) throw new import_meta_whatsapp_contracts7.InvalidWebhookSignatureError();
1154
+ }
1155
+ __name(verifyWebhookSignature, "verifyWebhookSignature");
1156
+ async function claimWebhookDelivery(params) {
1157
+ const key = `meta-whatsapp:webhook:${params.signatureHeader}`;
1158
+ return params.nonceStore.setIfAbsent(key, params.ttlSeconds ?? WEBHOOK_NONCE_TTL_SECONDS);
1159
+ }
1160
+ __name(claimWebhookDelivery, "claimWebhookDelivery");
1161
+
1162
+ // src/channel/ReceiveWebhook.use-case.ts
1163
+ function toSessionContract(row) {
1164
+ return {
1165
+ id: row.id,
1166
+ companyId: row.companyId,
1167
+ whatsappNumber: row.whatsappNumber,
1168
+ currentState: row.currentState,
1169
+ context: row.context,
1170
+ mode: row.mode,
1171
+ assignedUserId: row.assignedUserId,
1172
+ humanRequestedAt: row.humanRequestedAt?.toISOString() ?? null,
1173
+ lastInboundAt: row.lastInboundAt?.toISOString() ?? null,
1174
+ lastAgentReadAt: row.lastAgentReadAt?.toISOString() ?? null,
1175
+ lastActivity: row.lastActivity.toISOString(),
1176
+ createdAt: row.createdAt.toISOString(),
1177
+ updatedAt: row.updatedAt.toISOString()
1178
+ };
1179
+ }
1180
+ __name(toSessionContract, "toSessionContract");
1181
+ function extractContent(message) {
1182
+ if (message.text?.body) return message.text.body;
1183
+ const interactive = message.interactive;
1184
+ if (interactive?.button_reply) return interactive.button_reply.title || interactive.button_reply.id;
1185
+ if (interactive?.list_reply) return interactive.list_reply.title || interactive.list_reply.id;
1186
+ if (message.order) {
1187
+ const items = message.order.product_items.reduce((sum, item) => sum + item.quantity, 0);
1188
+ return `Pedido: ${items} item(ns)`;
1189
+ }
1190
+ return message.image?.caption ?? message.document?.caption ?? null;
1191
+ }
1192
+ __name(extractContent, "extractContent");
1193
+ function extractAnswer(message) {
1194
+ const interactive = message.interactive;
1195
+ if (interactive?.button_reply) return interactive.button_reply.id;
1196
+ if (interactive?.list_reply) return interactive.list_reply.id;
1197
+ return message.text?.body;
1198
+ }
1199
+ __name(extractAnswer, "extractAnswer");
1200
+ function extractPayload(message) {
1201
+ const payload = {};
1202
+ if (message.interactive) payload["interactive"] = message.interactive;
1203
+ if (message.order) payload["order"] = message.order;
1204
+ if (message.image) payload["image"] = message.image;
1205
+ if (message.audio) payload["audio"] = message.audio;
1206
+ if (message.video) payload["video"] = message.video;
1207
+ if (message.document) payload["document"] = message.document;
1208
+ if (message.sticker) payload["sticker"] = message.sticker;
1209
+ if (message.context?.referred_product) payload["referredProduct"] = message.context.referred_product;
1210
+ return Object.keys(payload).length > 0 ? payload : null;
1211
+ }
1212
+ __name(extractPayload, "extractPayload");
1213
+ var ReceiveWebhookUseCase = class {
1214
+ static {
1215
+ __name(this, "ReceiveWebhookUseCase");
1216
+ }
1217
+ params;
1218
+ constructor(params) {
1219
+ this.params = params;
1220
+ }
1221
+ async execute(input) {
1222
+ verifyWebhookSignature({
1223
+ rawBody: input.rawBody,
1224
+ signatureHeader: input.signatureHeader,
1225
+ appSecret: this.params.appSecret
1226
+ });
1227
+ const claimed = await claimWebhookDelivery({
1228
+ nonceStore: this.params.nonceStore,
1229
+ signatureHeader: input.signatureHeader
1230
+ });
1231
+ if (!claimed) return {
1232
+ duplicate: true,
1233
+ messagesProcessed: 0,
1234
+ statusesProcessed: 0
1235
+ };
1236
+ const rawText = typeof input.rawBody === "string" ? input.rawBody : input.rawBody.toString("utf8");
1237
+ const payload = import_meta_whatsapp_contracts8.whatsAppWebhookPayloadSchema.parse(JSON.parse(rawText));
1238
+ let messagesProcessed = 0;
1239
+ let statusesProcessed = 0;
1240
+ for (const entry of payload.entry) {
1241
+ for (const change of entry.changes) {
1242
+ for (const message of change.value.messages ?? []) {
1243
+ await this.handleMessage(input.companyId, message);
1244
+ messagesProcessed++;
1245
+ }
1246
+ for (const status of change.value.statuses ?? []) {
1247
+ await this.handleStatus(input.companyId, status);
1248
+ statusesProcessed++;
1249
+ }
1250
+ }
1251
+ }
1252
+ return {
1253
+ duplicate: false,
1254
+ messagesProcessed,
1255
+ statusesProcessed
1256
+ };
1257
+ }
1258
+ async handleMessage(companyId, message) {
1259
+ const saved = await this.params.logMessage.execute({
1260
+ companyId,
1261
+ whatsappNumber: message.from,
1262
+ direction: "inbound",
1263
+ sender: "customer",
1264
+ type: message.type,
1265
+ content: extractContent(message),
1266
+ payload: extractPayload(message),
1267
+ waMessageId: message.id,
1268
+ status: "received",
1269
+ startState: this.params.startState
1270
+ });
1271
+ if (!saved) return;
1272
+ const sessionRow = await this.params.sessionRepository.getContext(companyId, message.from);
1273
+ if (!sessionRow) return;
1274
+ if (sessionRow.mode === "human") return;
1275
+ const outcome = await this.params.hooks?.onMessageReceived?.(message, toSessionContract(sessionRow));
1276
+ if (outcome?.outcome === "handled") return;
1277
+ void extractAnswer(message);
1278
+ }
1279
+ async handleStatus(companyId, status) {
1280
+ const updated = await this.params.messageRepository.updateMessageStatus(companyId, status.id, status.status);
1281
+ if (!updated) return;
1282
+ this.params.realtime?.emit(`conv:${updated.whatsappNumber}`, "message-status", {
1283
+ waMessageId: status.id,
1284
+ status: status.status
1285
+ });
1286
+ const sessionRow = await this.params.sessionRepository.getContext(companyId, updated.whatsappNumber);
1287
+ await this.params.hooks?.onStatusUpdate?.(status, sessionRow ? toSessionContract(sessionRow) : null);
1288
+ }
1289
+ };
1290
+
1291
+ // src/channel/IngestInboundMedia.use-case.ts
1292
+ var import_drizzle_orm6 = require("drizzle-orm");
1293
+ var IngestInboundMediaUseCase = class {
1294
+ static {
1295
+ __name(this, "IngestInboundMediaUseCase");
1296
+ }
1297
+ db;
1298
+ channel;
1299
+ objectStorage;
1300
+ constructor(db, channel, objectStorage) {
1301
+ this.db = db;
1302
+ this.channel = channel;
1303
+ this.objectStorage = objectStorage;
1304
+ }
1305
+ async execute(params) {
1306
+ const [message] = await this.db.select().from(messages).where((0, import_drizzle_orm6.and)((0, import_drizzle_orm6.eq)(messages.companyId, params.companyId), (0, import_drizzle_orm6.eq)(messages.id, params.messageId))).limit(1);
1307
+ if (!message) throw new Error(`Mensagem ${params.messageId} n\xE3o encontrada para ingest\xE3o de m\xEDdia`);
1308
+ const payload = message.payload ?? {};
1309
+ if (payload["uploadId"] && payload["sourceMediaId"] === params.sourceMediaId) {
1310
+ return {
1311
+ uploadId: String(payload["uploadId"]),
1312
+ alreadyIngested: true
1313
+ };
1314
+ }
1315
+ const { data, mimeType } = await this.channel.fetchMediaAsBase64(params.sourceMediaId);
1316
+ const buffer = Buffer.from(data, "base64");
1317
+ const { uploadId } = await this.objectStorage.upload({
1318
+ buffer,
1319
+ mimeType: mimeType || params.mimeType,
1320
+ key: `meta-whatsapp/${params.companyId}/inbound/${params.sourceMediaId}`
1321
+ });
1322
+ const updatedPayload = {
1323
+ ...payload,
1324
+ uploadId,
1325
+ sourceMediaId: params.sourceMediaId,
1326
+ mimeType: mimeType || params.mimeType,
1327
+ ...params.filename ? {
1328
+ filename: params.filename
1329
+ } : {}
1330
+ };
1331
+ await this.db.update(messages).set({
1332
+ payload: updatedPayload
1333
+ }).where((0, import_drizzle_orm6.and)((0, import_drizzle_orm6.eq)(messages.companyId, params.companyId), (0, import_drizzle_orm6.eq)(messages.id, params.messageId)));
1334
+ return {
1335
+ uploadId,
1336
+ alreadyIngested: false
1337
+ };
1338
+ }
1339
+ };
1340
+ function extractMediaDescriptor(message) {
1341
+ const payload = message.payload ?? {};
1342
+ for (const key of [
1343
+ "image",
1344
+ "audio",
1345
+ "video",
1346
+ "document",
1347
+ "sticker"
1348
+ ]) {
1349
+ const media = payload[key];
1350
+ if (media?.id) {
1351
+ return {
1352
+ sourceMediaId: media.id,
1353
+ mimeType: media.mime_type ?? "application/octet-stream",
1354
+ filename: media.filename
1355
+ };
1356
+ }
1357
+ }
1358
+ return void 0;
1359
+ }
1360
+ __name(extractMediaDescriptor, "extractMediaDescriptor");
1361
+
1362
+ // src/createMetaWhatsAppModule.ts
1363
+ function createMetaWhatsAppModule(params) {
1364
+ const { db, config, nonceStore, providers = {}, hooks } = params;
1365
+ const startState = params.startState ?? "start";
1366
+ const flowEngineEnabled = params.features?.flowEngine ?? true;
1367
+ const messageProvider = new import_meta_whatsapp_provider.WhatsAppMessageProvider({
1368
+ accessToken: config.accessToken,
1369
+ phoneNumberId: config.phoneNumberId,
1370
+ wabaId: config.wabaId,
1371
+ apiVersion: config.apiVersion,
1372
+ baseUrl: config.baseUrl
1373
+ });
1374
+ const channel = new WhatsAppChannelAdapter(messageProvider);
1375
+ const sessionRepository = new SessionRepository(db);
1376
+ const messageRepository = new MessageRepository(db);
1377
+ const settingsRepository = new SettingsRepository(db);
1378
+ const flowGraphRepository = new FlowGraphRepository(db);
1379
+ const logMessage = new LogMessageUseCase(sessionRepository, messageRepository, providers.realtime);
1380
+ const sendMessage = new SendMessageUseCase(channel, sessionRepository, logMessage, providers.objectStorage);
1381
+ const receiveWebhook = new ReceiveWebhookUseCase({
1382
+ appSecret: config.appSecret,
1383
+ nonceStore,
1384
+ sessionRepository,
1385
+ messageRepository,
1386
+ logMessage,
1387
+ startState,
1388
+ hooks,
1389
+ realtime: providers.realtime
1390
+ });
1391
+ const flowInterpreter = flowEngineEnabled ? new FlowInterpreter() : void 0;
1392
+ const ingestInboundMedia = providers.objectStorage ? new IngestInboundMediaUseCase(db, channel, providers.objectStorage) : void 0;
1393
+ return {
1394
+ channel,
1395
+ // undefined quando providers.objectStorage não foi injetado.
1396
+ ingestInboundMedia,
1397
+ conversations: {
1398
+ log: logMessage,
1399
+ send: sendMessage,
1400
+ takeover: new TakeoverConversationUseCase(sessionRepository, providers.realtime),
1401
+ release: new ReleaseConversationUseCase(sessionRepository, providers.realtime),
1402
+ list: new ListConversationsUseCase(sessionRepository),
1403
+ listMessages: new ListMessagesUseCase(sessionRepository, messageRepository),
1404
+ export: new ExportConversationUseCase(sessionRepository),
1405
+ repository: sessionRepository
1406
+ },
1407
+ settings: settingsRepository,
1408
+ webhook: {
1409
+ receive: receiveWebhook,
1410
+ // GET de verificação da Meta — o host liga na sua rota e devolve o retorno como texto puro.
1411
+ verifyChallenge: /* @__PURE__ */ __name((query) => verifyWebhookChallenge({
1412
+ ...query,
1413
+ expectedToken: config.webhookVerifyToken
1414
+ }), "verifyChallenge")
1415
+ },
1416
+ // undefined quando features.flowEngine === false.
1417
+ flows: flowInterpreter ? {
1418
+ interpreter: flowInterpreter,
1419
+ registerFlowAction: /* @__PURE__ */ __name((kind, handler) => flowInterpreter.registerFlowAction(kind, handler), "registerFlowAction"),
1420
+ get: new GetFlowGraphUseCase(flowGraphRepository),
1421
+ list: new ListFlowGraphsUseCase(flowGraphRepository),
1422
+ create: new CreateFlowGraphUseCase(flowGraphRepository),
1423
+ save: new SaveFlowGraphUseCase(flowGraphRepository),
1424
+ delete: new DeleteFlowGraphUseCase(flowGraphRepository),
1425
+ livePositions: new GetLiveFlowPositionsUseCase(flowGraphRepository),
1426
+ repository: flowGraphRepository
1427
+ } : void 0,
1428
+ catalog: providers.catalog
1429
+ };
1430
+ }
1431
+ __name(createMetaWhatsAppModule, "createMetaWhatsAppModule");
1432
+
1433
+ // src/runMigrations.ts
1434
+ var import_node_path = require("path");
1435
+ var import_migrator = require("drizzle-orm/bun-sql/migrator");
1436
+ async function runMetaWhatsAppMigrations(db) {
1437
+ await (0, import_migrator.migrate)(db, {
1438
+ migrationsFolder: (0, import_node_path.join)(__dirname, "migrations"),
1439
+ migrationsTable: "meta_whatsapp_migrations"
1440
+ });
1441
+ }
1442
+ __name(runMetaWhatsAppMigrations, "runMetaWhatsAppMigrations");
1443
+
1444
+ // src/realtime/SseHub.ts
1445
+ var import_node_crypto2 = require("crypto");
1446
+ var TICKET_TTL_SECONDS = 60;
1447
+ var SseHub = class {
1448
+ static {
1449
+ __name(this, "SseHub");
1450
+ }
1451
+ relay;
1452
+ localListeners = /* @__PURE__ */ new Map();
1453
+ // Uma assinatura de relay por CANAL, compartilhada por todos os listeners locais dele — uma
1454
+ // por listener faria o relay entregar N cópias num canal com N conexões abertas.
1455
+ relaySubscriptions = /* @__PURE__ */ new Map();
1456
+ // Identifica esta instância do processo para descartar o próprio eco (pub/sub entrega a
1457
+ // mensagem a todos os inscritos, inclusive quem publicou).
1458
+ instanceId = (0, import_node_crypto2.randomUUID)();
1459
+ constructor(relay) {
1460
+ this.relay = relay;
1461
+ }
1462
+ emit(channel, event, payload) {
1463
+ this.dispatchLocal(channel, event, payload);
1464
+ if (this.relay) {
1465
+ const envelope = {
1466
+ originId: this.instanceId,
1467
+ event,
1468
+ payload
1469
+ };
1470
+ this.relay.publish(channel, JSON.stringify(envelope)).catch(() => void 0);
1471
+ }
1472
+ }
1473
+ dispatchLocal(channel, event, payload) {
1474
+ const listeners = this.localListeners.get(channel);
1475
+ if (!listeners) return;
1476
+ for (const listener of listeners) listener(event, payload);
1477
+ }
1478
+ async subscribe(channel, listener) {
1479
+ if (!this.localListeners.has(channel)) this.localListeners.set(channel, /* @__PURE__ */ new Set());
1480
+ this.localListeners.get(channel).add(listener);
1481
+ if (this.relay && !this.relaySubscriptions.has(channel)) {
1482
+ this.relaySubscriptions.set(channel, this.relay.subscribe(channel, (message) => {
1483
+ try {
1484
+ const envelope = JSON.parse(message);
1485
+ if (envelope.originId === this.instanceId) return;
1486
+ this.dispatchLocal(channel, envelope.event, envelope.payload);
1487
+ } catch {
1488
+ }
1489
+ }));
1490
+ }
1491
+ return () => {
1492
+ const listeners = this.localListeners.get(channel);
1493
+ listeners?.delete(listener);
1494
+ if (listeners && listeners.size === 0) {
1495
+ this.localListeners.delete(channel);
1496
+ const pendingUnsubscribe = this.relaySubscriptions.get(channel);
1497
+ this.relaySubscriptions.delete(channel);
1498
+ pendingUnsubscribe?.then((unsubscribe) => unsubscribe()).catch(() => void 0);
1499
+ }
1500
+ };
1501
+ }
1502
+ };
1503
+ async function issueSseTicket(store, companyId, whatsappNumber) {
1504
+ const ticket = (0, import_node_crypto2.randomUUID)();
1505
+ await store.set(`sse:ticket:${ticket}`, JSON.stringify({
1506
+ companyId,
1507
+ whatsappNumber
1508
+ }), TICKET_TTL_SECONDS);
1509
+ return ticket;
1510
+ }
1511
+ __name(issueSseTicket, "issueSseTicket");
1512
+ async function redeemSseTicket(store, ticket) {
1513
+ const raw = await store.get(`sse:ticket:${ticket}`);
1514
+ if (!raw) return null;
1515
+ await store.delete(`sse:ticket:${ticket}`);
1516
+ return JSON.parse(raw);
1517
+ }
1518
+ __name(redeemSseTicket, "redeemSseTicket");
1519
+ // Annotate the CommonJS export names for ESM import in node:
1520
+ 0 && (module.exports = {
1521
+ CreateFlowGraphUseCase,
1522
+ DeleteFlowGraphUseCase,
1523
+ ExportConversationUseCase,
1524
+ FlowGraphRepository,
1525
+ FlowInterpreter,
1526
+ GetFlowGraphUseCase,
1527
+ GetLiveFlowPositionsUseCase,
1528
+ IngestInboundMediaUseCase,
1529
+ InvalidFlowGraphError,
1530
+ ListConversationsUseCase,
1531
+ ListFlowGraphsUseCase,
1532
+ ListMessagesUseCase,
1533
+ LogMessageUseCase,
1534
+ MessageRepository,
1535
+ OptimisticLockError,
1536
+ ReceiveWebhookUseCase,
1537
+ ReleaseConversationUseCase,
1538
+ SaveFlowGraphUseCase,
1539
+ SendMessageUseCase,
1540
+ SessionRepository,
1541
+ SettingsRepository,
1542
+ SseHub,
1543
+ TakeoverConversationUseCase,
1544
+ WEBHOOK_NONCE_TTL_SECONDS,
1545
+ WhatsAppChannelAdapter,
1546
+ claimWebhookDelivery,
1547
+ createMetaWhatsAppModule,
1548
+ extractMediaDescriptor,
1549
+ flowGraphs,
1550
+ issueSseTicket,
1551
+ messages,
1552
+ metaWhatsAppSchema,
1553
+ redeemSseTicket,
1554
+ runMetaWhatsAppMigrations,
1555
+ sessions,
1556
+ settings,
1557
+ verifyWebhookChallenge,
1558
+ verifyWebhookSignature
1559
+ });
1560
+ //# sourceMappingURL=index.cjs.map