@omnizap-system/omnizap 2.6.2 → 2.6.3

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.
Files changed (48) hide show
  1. package/.env.example +24 -0
  2. package/app/config/index.js +4 -0
  3. package/app/configParts/adminIdentity.js +29 -0
  4. package/app/configParts/baileysConfig.js +116 -0
  5. package/app/configParts/groupUtils.js +221 -0
  6. package/app/configParts/loggerConfig.js +185 -0
  7. package/app/configParts/messagePersistenceService.js +169 -7
  8. package/app/configParts/sessionConfig.js +85 -0
  9. package/app/connection/baileysCompatibility.test.js +9 -0
  10. package/app/connection/baileysDbAuthState.js +205 -9
  11. package/app/connection/baileysLibsignalPatch.js +210 -0
  12. package/app/connection/groupOwnerWriteStateResolver.js +53 -21
  13. package/app/connection/socketController.js +95 -25
  14. package/app/connection/socketController.multiSession.test.js +20 -0
  15. package/app/controllers/messagePipeline/preProcessingMiddlewares.js +17 -3
  16. package/app/controllers/messageProcessingPipeline.js +2 -0
  17. package/app/controllers/messageProcessingPipeline.test.js +15 -13
  18. package/app/services/multiSession/assignmentBalancerService.js +1 -6
  19. package/app/services/multiSession/groupOwnershipRepository.js +9 -44
  20. package/app/services/multiSession/groupOwnershipService.js +9 -90
  21. package/app/services/multiSession/groupOwnershipService.test.js +12 -4
  22. package/app/services/multiSession/sessionRegistryService.js +6 -60
  23. package/app/utils/antiLink/antiLinkModule.js +54 -24
  24. package/docs/security/omnizap-static-security-headers.conf +3 -3
  25. package/package.json +3 -2
  26. package/public/comandos/commands-catalog.json +1 -1
  27. package/public/css/payments-react.css +478 -0
  28. package/public/js/apps/homeReactApp.js +2 -2
  29. package/public/js/apps/paymentsCancelReactApp.js +45 -0
  30. package/public/js/apps/paymentsReactApp.js +399 -0
  31. package/public/js/apps/paymentsSuccessReactApp.js +148 -0
  32. package/public/pages/pagamentos-cancelado.html +21 -0
  33. package/public/pages/pagamentos-sucesso.html +21 -0
  34. package/public/pages/pagamentos.html +30 -0
  35. package/scripts/deploy.sh +3 -0
  36. package/scripts/new-whatsapp-session.sh +247 -0
  37. package/server/controllers/admin/systemAdminController.js +4 -17
  38. package/server/controllers/payments/paymentsController.js +731 -0
  39. package/server/controllers/system/systemController.js +4 -30
  40. package/server/email/emailAutomationRuntime.js +36 -1
  41. package/server/email/emailAutomationService.js +42 -1
  42. package/server/email/emailTemplateService.js +137 -31
  43. package/server/http/httpRequestUtils.js +18 -14
  44. package/server/middleware/securityHeaders.js +15 -2
  45. package/server/routes/indexRouter.js +27 -7
  46. package/server/routes/payments/paymentsRouter.js +47 -0
  47. package/server/routes/static/staticPageRouter.js +3 -0
  48. package/vite.config.mjs +3 -0
@@ -1,8 +1,30 @@
1
+ /**
2
+ * Sessão padrão usada como fallback.
3
+ * @type {string}
4
+ */
1
5
  const DEFAULT_SESSION_ID = 'default';
6
+ /**
7
+ * Tamanho máximo permitido para IDs de sessão.
8
+ * @type {number}
9
+ */
2
10
  const SESSION_ID_MAX_LENGTH = 64;
11
+ /**
12
+ * Regex de validação para IDs de sessão.
13
+ * @type {RegExp}
14
+ */
3
15
  const SESSION_ID_PATTERN = /^[a-zA-Z0-9:_-]+$/;
16
+ /**
17
+ * Modos de enforcement válidos para ownership de grupo.
18
+ * @type {Set<string>}
19
+ */
4
20
  const OWNER_ENFORCEMENT_MODES = new Set(['off', 'shadow', 'enforce']);
5
21
 
22
+ /**
23
+ * Converte um valor de ambiente para boolean com fallback.
24
+ * @param {unknown} value
25
+ * @param {boolean} fallback
26
+ * @returns {boolean}
27
+ */
6
28
  const parseEnvBool = (value, fallback) => {
7
29
  if (value === undefined || value === null || value === '') return fallback;
8
30
  const normalized = String(value).trim().toLowerCase();
@@ -11,20 +33,43 @@ const parseEnvBool = (value, fallback) => {
11
33
  return fallback;
12
34
  };
13
35
 
36
+ /**
37
+ * Converte um valor em inteiro com limites.
38
+ * @param {unknown} value
39
+ * @param {number} fallback
40
+ * @param {number} min
41
+ * @param {number} max
42
+ * @returns {number}
43
+ */
14
44
  const parseEnvInt = (value, fallback, min, max) => {
15
45
  const parsed = Number(value);
16
46
  if (!Number.isFinite(parsed)) return fallback;
17
47
  return Math.max(min, Math.min(max, Math.floor(parsed)));
18
48
  };
19
49
 
50
+ /**
51
+ * Faz parse de entradas separadas por vírgula, quebra de linha ou `;`.
52
+ * @param {unknown} value
53
+ * @returns {string[]}
54
+ */
20
55
  const parseFlexibleEntries = (value) =>
21
56
  String(value || '')
22
57
  .split(/[,\n;]+/g)
23
58
  .map((entry) => String(entry || '').trim())
24
59
  .filter(Boolean);
25
60
 
61
+ /**
62
+ * Normaliza um ID de sessão.
63
+ * @param {unknown} value
64
+ * @returns {string}
65
+ */
26
66
  const normalizeSessionId = (value) => String(value || '').trim();
27
67
 
68
+ /**
69
+ * Valida formato e tamanho de ID de sessão.
70
+ * @param {unknown} value
71
+ * @returns {boolean}
72
+ */
28
73
  const isValidSessionId = (value) => {
29
74
  const normalized = normalizeSessionId(value);
30
75
  if (!normalized) return false;
@@ -32,6 +77,14 @@ const isValidSessionId = (value) => {
32
77
  return SESSION_ID_PATTERN.test(normalized);
33
78
  };
34
79
 
80
+ /**
81
+ * @typedef {{sessionIds: string[], warnings: string[]}} ParsedSessionIds
82
+ */
83
+ /**
84
+ * Interpreta lista de sessões do ambiente e aplica fallback legado.
85
+ * @param {{sessionIdsRaw?: string, legacySessionIdRaw?: string}} [params]
86
+ * @returns {ParsedSessionIds}
87
+ */
35
88
  const parseSessionIds = ({ sessionIdsRaw = '', legacySessionIdRaw = '' } = {}) => {
36
89
  const warnings = [];
37
90
  const validSessionIds = [];
@@ -63,6 +116,13 @@ const parseSessionIds = ({ sessionIdsRaw = '', legacySessionIdRaw = '' } = {}) =
63
116
  };
64
117
  };
65
118
 
119
+ /**
120
+ * Resolve pesos por sessão com validação e defaults.
121
+ * @param {unknown} rawValue
122
+ * @param {string[]} sessionIds
123
+ * @param {string[]} warnings
124
+ * @returns {Record<string, number>}
125
+ */
66
126
  const parseSessionWeights = (rawValue, sessionIds, warnings) => {
67
127
  const allowedSessions = new Set(sessionIds);
68
128
  const weights = {};
@@ -102,6 +162,23 @@ const parseSessionWeights = (rawValue, sessionIds, warnings) => {
102
162
  return weights;
103
163
  };
104
164
 
165
+ /**
166
+ * @typedef {{
167
+ * sessionIds: readonly string[],
168
+ * primarySessionId: string,
169
+ * sessionWeights: Readonly<Record<string, number>>,
170
+ * ownerEnforcementMode: 'off'|'shadow'|'enforce',
171
+ * ownerLeaseMs: number,
172
+ * ownerHeartbeatMs: number,
173
+ * balancerEnabled: boolean,
174
+ * warnings: readonly string[]
175
+ * }} MultiSessionRuntimeConfig
176
+ */
177
+ /**
178
+ * Resolve a configuração de runtime para múltiplas sessões.
179
+ * @param {Record<string, any>} [env=process.env]
180
+ * @returns {MultiSessionRuntimeConfig}
181
+ */
105
182
  export const resolveMultiSessionRuntimeConfig = (env = process.env) => {
106
183
  const warnings = [];
107
184
  const legacySessionId = normalizeSessionId(env.BAILEYS_AUTH_SESSION_ID) || DEFAULT_SESSION_ID;
@@ -152,6 +229,14 @@ export const resolveMultiSessionRuntimeConfig = (env = process.env) => {
152
229
  });
153
230
  };
154
231
 
232
+ /**
233
+ * Configuração resolvida uma única vez por processo.
234
+ * @type {MultiSessionRuntimeConfig}
235
+ */
155
236
  export const multiSessionRuntimeConfig = resolveMultiSessionRuntimeConfig();
156
237
 
238
+ /**
239
+ * Retorna a configuração de runtime multi-sessão.
240
+ * @returns {MultiSessionRuntimeConfig}
241
+ */
157
242
  export const getMultiSessionRuntimeConfig = () => multiSessionRuntimeConfig;
@@ -6,12 +6,21 @@ import test from 'node:test';
6
6
 
7
7
  import { initAuthCreds, proto } from '@whiskeysockets/baileys';
8
8
 
9
+ /**
10
+ * Referência do fork/branch do Baileys validada pelos testes.
11
+ * @type {string}
12
+ */
9
13
  const PINNED_BAILEYS_REF = 'github:jlucaso1/Baileys#feat-add-stickerpack-support';
10
14
 
11
15
  const require = createRequire(import.meta.url);
12
16
  const baileysPackageJsonPath = require.resolve('@whiskeysockets/baileys/package.json');
13
17
  const baileysPackageDir = path.dirname(baileysPackageJsonPath);
14
18
 
19
+ /**
20
+ * Lê um arquivo de tipos dentro do pacote instalado do Baileys.
21
+ * @param {string} relativePath
22
+ * @returns {Promise<string>}
23
+ */
15
24
  const readBaileysTypeFile = async (relativePath) => readFile(path.join(baileysPackageDir, relativePath), 'utf8');
16
25
 
17
26
  test('Auth.d.ts expõe AuthenticationState compatível com SocketConfig.auth', async () => {
@@ -1,19 +1,69 @@
1
- import { initAuthCreds, proto } from '@whiskeysockets/baileys';
1
+ import { initAuthCreds, makeCacheableSignalKeyStore, proto } from '@whiskeysockets/baileys';
2
2
  import { readdir, readFile } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
 
5
5
  import { baileysAuthLogger as logger } from '../config/index.js';
6
6
  import { TABLES, executeQuery, pool } from '../../database/index.js';
7
7
 
8
+ /**
9
+ * Nome da tabela que persiste o estado de autenticação do Baileys.
10
+ * @type {string}
11
+ */
8
12
  const AUTH_TABLE = TABLES.BAILEYS_AUTH_STATE;
13
+ /**
14
+ * Categoria usada para armazenar as credenciais principais.
15
+ * @type {string}
16
+ */
9
17
  const CREDS_CATEGORY = 'creds';
18
+ /**
19
+ * Identificador fixo da linha de credenciais.
20
+ * @type {string}
21
+ */
10
22
  const CREDS_ITEM_ID = 'default';
23
+ /**
24
+ * Extensão esperada para arquivos de bootstrap de auth state.
25
+ * @type {string}
26
+ */
11
27
  const AUTH_FILE_EXTENSION = '.json';
28
+ /**
29
+ * Tipos conhecidos de signal keys persistidos no auth state.
30
+ * @type {string[]}
31
+ */
12
32
  const KNOWN_SIGNAL_KEY_TYPES = ['pre-key', 'session', 'sender-key', 'sender-key-memory', 'app-state-sync-key', 'app-state-sync-version', 'lid-mapping', 'device-list', 'tctoken'];
33
+ /**
34
+ * Tipos ordenados por tamanho (desc) para priorizar match de prefixo mais específico.
35
+ * @type {string[]}
36
+ */
13
37
  const KNOWN_SIGNAL_KEY_TYPES_SORTED = [...KNOWN_SIGNAL_KEY_TYPES].sort((left, right) => right.length - left.length);
38
+ /**
39
+ * Interpreta uma variável de ambiente booleana com fallback.
40
+ * @param {unknown} value
41
+ * @param {boolean} fallback
42
+ * @returns {boolean}
43
+ */
44
+ const parseEnvBool = (value, fallback) => {
45
+ if (value === undefined || value === null || value === '') return fallback;
46
+ const normalized = String(value).trim().toLowerCase();
47
+ if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
48
+ if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
49
+ return fallback;
50
+ };
51
+ /**
52
+ * Habilita cache em memória do key store do Baileys.
53
+ * @type {boolean}
54
+ */
55
+ const BAILEYS_AUTH_KEYS_CACHE_ENABLED = parseEnvBool(process.env.BAILEYS_AUTH_KEYS_CACHE_ENABLED, true);
14
56
 
57
+ /**
58
+ * Promise compartilhada para inicialização idempotente da tabela.
59
+ * @type {Promise<void> | null}
60
+ */
15
61
  let ensureTablePromise = null;
16
62
 
63
+ /**
64
+ * Helpers de serialização JSON que preservam Buffers/Uint8Array.
65
+ * @type {{replacer: (key: string, value: any) => any, reviver: (key: string, value: any) => any}}
66
+ */
17
67
  const BufferJSON = {
18
68
  replacer: (_, value) => {
19
69
  if (Buffer.isBuffer(value) || value instanceof Uint8Array || value?.type === 'Buffer') {
@@ -38,20 +88,45 @@ const BufferJSON = {
38
88
  },
39
89
  };
40
90
 
91
+ /**
92
+ * Monta placeholder SQL para cláusula `IN`.
93
+ * @param {number} count
94
+ * @returns {string}
95
+ */
41
96
  const buildInClause = (count) => new Array(count).fill('?').join(', ');
42
97
 
98
+ /**
99
+ * Normaliza o ID de sessão do auth state.
100
+ * @param {string | null | undefined} sessionId
101
+ * @returns {string}
102
+ */
43
103
  const normalizeSessionId = (sessionId) => {
44
104
  const normalized = String(sessionId || '').trim();
45
105
  return normalized || 'default';
46
106
  };
47
107
 
108
+ /**
109
+ * Normaliza o identificador de item para armazenamento no banco.
110
+ * @param {string | null | undefined} value
111
+ * @returns {string}
112
+ */
48
113
  const normalizeStorageId = (value) =>
49
114
  String(value || '')
50
115
  .replace(/\//g, '__')
51
116
  .replace(/:/g, '-');
52
117
 
118
+ /**
119
+ * Serializa payload para coluna JSON com suporte a Buffer.
120
+ * @param {any} value
121
+ * @returns {string}
122
+ */
53
123
  const toJsonPayload = (value) => JSON.stringify(value, BufferJSON.replacer);
54
124
 
125
+ /**
126
+ * Faz parse de payload JSON persistido no banco.
127
+ * @param {unknown} rawPayload
128
+ * @returns {any | null}
129
+ */
55
130
  const parseJsonPayload = (rawPayload) => {
56
131
  if (rawPayload === null || rawPayload === undefined) return null;
57
132
  try {
@@ -65,6 +140,58 @@ const parseJsonPayload = (rawPayload) => {
65
140
  }
66
141
  };
67
142
 
143
+ /**
144
+ * @typedef {{
145
+ * totalRows: number,
146
+ * credsRows: number,
147
+ * signalKeyRows: number,
148
+ * categories: Record<string, number>
149
+ * }} SessionAuthStateStats
150
+ */
151
+ /**
152
+ * Lê estatísticas agregadas do auth state para a sessão.
153
+ * @param {string} sessionId
154
+ * @returns {Promise<SessionAuthStateStats>}
155
+ */
156
+ const readSessionAuthStateStats = async (sessionId) => {
157
+ const rows = await executeQuery(
158
+ `
159
+ SELECT category, COUNT(*) AS total
160
+ FROM \`${AUTH_TABLE}\`
161
+ WHERE session_id = ?
162
+ GROUP BY category
163
+ `,
164
+ [sessionId],
165
+ );
166
+
167
+ const stats = {
168
+ totalRows: 0,
169
+ credsRows: 0,
170
+ signalKeyRows: 0,
171
+ categories: {},
172
+ };
173
+
174
+ for (const row of rows || []) {
175
+ const category = String(row?.category || '').trim();
176
+ const total = Number(row?.total || 0);
177
+ if (!category || total <= 0) continue;
178
+
179
+ stats.totalRows += total;
180
+ stats.categories[category] = total;
181
+ if (category === CREDS_CATEGORY) {
182
+ stats.credsRows += total;
183
+ } else {
184
+ stats.signalKeyRows += total;
185
+ }
186
+ }
187
+
188
+ return stats;
189
+ };
190
+
191
+ /**
192
+ * Garante a existência da tabela de auth state no banco.
193
+ * @returns {Promise<void>}
194
+ */
68
195
  const ensureAuthStateTable = async () => {
69
196
  if (ensureTablePromise) {
70
197
  return ensureTablePromise;
@@ -99,11 +226,15 @@ const ensureAuthStateTable = async () => {
99
226
  return ensureTablePromise;
100
227
  };
101
228
 
102
- const hasSessionData = async (sessionId) => {
103
- const rows = await executeQuery(`SELECT 1 FROM \`${AUTH_TABLE}\` WHERE session_id = ? LIMIT 1`, [sessionId]);
104
- return Array.isArray(rows) && rows.length > 0;
105
- };
106
-
229
+ /**
230
+ * Insere ou atualiza uma linha de auth state.
231
+ * @param {string} sessionId
232
+ * @param {string} category
233
+ * @param {string} itemId
234
+ * @param {any} value
235
+ * @param {import('mysql2/promise').PoolConnection | null} [connection=null]
236
+ * @returns {Promise<void>}
237
+ */
107
238
  const upsertAuthRow = async (sessionId, category, itemId, value, connection = null) => {
108
239
  const payload = toJsonPayload(value);
109
240
  await executeQuery(
@@ -117,10 +248,23 @@ const upsertAuthRow = async (sessionId, category, itemId, value, connection = nu
117
248
  );
118
249
  };
119
250
 
251
+ /**
252
+ * Remove uma linha de auth state.
253
+ * @param {string} sessionId
254
+ * @param {string} category
255
+ * @param {string} itemId
256
+ * @param {import('mysql2/promise').PoolConnection | null} [connection=null]
257
+ * @returns {Promise<void>}
258
+ */
120
259
  const deleteAuthRow = async (sessionId, category, itemId, connection = null) => {
121
260
  await executeQuery(`DELETE FROM \`${AUTH_TABLE}\` WHERE session_id = ? AND category = ? AND item_id = ?`, [sessionId, category, itemId], connection);
122
261
  };
123
262
 
263
+ /**
264
+ * Lê as credenciais salvas para uma sessão.
265
+ * @param {string} sessionId
266
+ * @returns {Promise<import('@whiskeysockets/baileys').AuthenticationCreds | null>}
267
+ */
124
268
  const readCredsFromDb = async (sessionId) => {
125
269
  const rows = await executeQuery(`SELECT payload FROM \`${AUTH_TABLE}\` WHERE session_id = ? AND category = ? AND item_id = ? LIMIT 1`, [sessionId, CREDS_CATEGORY, CREDS_ITEM_ID]);
126
270
  const payload = rows?.[0]?.payload;
@@ -128,6 +272,14 @@ const readCredsFromDb = async (sessionId) => {
128
272
  return parsed || null;
129
273
  };
130
274
 
275
+ /**
276
+ * @typedef {{category: string, itemId: string}} AuthFileMetadata
277
+ */
278
+ /**
279
+ * Extrai categoria e itemId de um arquivo legado de auth state.
280
+ * @param {string} fileName
281
+ * @returns {AuthFileMetadata | null}
282
+ */
131
283
  const parseAuthFileMetadata = (fileName) => {
132
284
  if (!fileName || typeof fileName !== 'string' || !fileName.endsWith(AUTH_FILE_EXTENSION)) {
133
285
  return null;
@@ -156,11 +308,31 @@ const parseAuthFileMetadata = (fileName) => {
156
308
  return null;
157
309
  };
158
310
 
311
+ /**
312
+ * Migra auth state legado de arquivos para MySQL, quando necessário.
313
+ * @param {string} sessionId
314
+ * @param {string | null} bootstrapFromDir
315
+ * @returns {Promise<boolean>} `true` quando houve importação de ao menos uma linha.
316
+ */
159
317
  const migrateSessionFromFiles = async (sessionId, bootstrapFromDir) => {
160
318
  if (!bootstrapFromDir) return false;
161
319
 
162
- const existsInDb = await hasSessionData(sessionId);
163
- if (existsInDb) return false;
320
+ const authStatsBeforeMigration = await readSessionAuthStateStats(sessionId);
321
+ const hasSessionData = authStatsBeforeMigration.totalRows > 0;
322
+ const hasSignalKeyRows = authStatsBeforeMigration.signalKeyRows > 0;
323
+ if (hasSessionData && hasSignalKeyRows) return false;
324
+
325
+ if (hasSessionData && !hasSignalKeyRows) {
326
+ logger.warn('Auth state do Baileys está parcial (somente creds) e tentará bootstrap via arquivos.', {
327
+ action: 'baileys_auth_db_partial_state_detected',
328
+ sessionId,
329
+ credsRows: authStatsBeforeMigration.credsRows,
330
+ signalKeyRows: authStatsBeforeMigration.signalKeyRows,
331
+ totalRows: authStatsBeforeMigration.totalRows,
332
+ bootstrapFromDir,
333
+ table: AUTH_TABLE,
334
+ });
335
+ }
164
336
 
165
337
  let directoryEntries = [];
166
338
  try {
@@ -224,6 +396,7 @@ const migrateSessionFromFiles = async (sessionId, bootstrapFromDir) => {
224
396
  sessionId,
225
397
  importedRows,
226
398
  skippedRows,
399
+ hadPartialState: hasSessionData && !hasSignalKeyRows,
227
400
  bootstrapFromDir,
228
401
  table: AUTH_TABLE,
229
402
  });
@@ -232,6 +405,14 @@ const migrateSessionFromFiles = async (sessionId, bootstrapFromDir) => {
232
405
  return importedRows > 0;
233
406
  };
234
407
 
408
+ /**
409
+ * Cria implementação de SignalKeyStore persistida em MySQL.
410
+ * @param {string} sessionId
411
+ * @returns {{
412
+ * get: (type: string, ids: string[]) => Promise<Record<string, any>>,
413
+ * set: (data: Record<string, Record<string, any>>) => Promise<void>
414
+ * }}
415
+ */
235
416
  const createDbSignalKeyStore = (sessionId) => ({
236
417
  /**
237
418
  * @param {string} type
@@ -330,12 +511,27 @@ export async function useDbAuthState(options = {}) {
330
511
  }
331
512
  }
332
513
 
514
+ const authStats = await readSessionAuthStateStats(sessionId);
515
+ if (authStats.totalRows > 0 && authStats.signalKeyRows === 0) {
516
+ logger.warn('Auth state do Baileys sem signal keys; sessão pode ficar instável até novo pareamento.', {
517
+ action: 'baileys_auth_db_missing_signal_keys',
518
+ sessionId,
519
+ credsRows: authStats.credsRows,
520
+ signalKeyRows: authStats.signalKeyRows,
521
+ totalRows: authStats.totalRows,
522
+ categories: authStats.categories,
523
+ table: AUTH_TABLE,
524
+ });
525
+ }
526
+
333
527
  const creds = (await readCredsFromDb(sessionId)) || initAuthCreds();
528
+ const keyStore = createDbSignalKeyStore(sessionId);
529
+ const wrappedKeyStore = BAILEYS_AUTH_KEYS_CACHE_ENABLED ? makeCacheableSignalKeyStore(keyStore, logger) : keyStore;
334
530
 
335
531
  return {
336
532
  state: {
337
533
  creds,
338
- keys: createDbSignalKeyStore(sessionId),
534
+ keys: wrappedKeyStore,
339
535
  },
340
536
  saveCreds: async () => {
341
537
  await upsertAuthRow(sessionId, CREDS_CATEGORY, CREDS_ITEM_ID, creds);
@@ -0,0 +1,210 @@
1
+ import logger from '#logger';
2
+ import { createRequire } from 'node:module';
3
+
4
+ const require = createRequire(import.meta.url);
5
+
6
+ /**
7
+ * Interpreta string/valor de ambiente como boolean.
8
+ * @param {unknown} value
9
+ * @param {boolean} fallback
10
+ * @returns {boolean}
11
+ */
12
+ const parseEnvBool = (value, fallback) => {
13
+ if (value === undefined || value === null || value === '') return fallback;
14
+ const normalized = String(value).trim().toLowerCase();
15
+ if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
16
+ if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
17
+ return fallback;
18
+ };
19
+
20
+ /**
21
+ * Habilita/desabilita patch runtime do libsignal.
22
+ * @type {boolean}
23
+ */
24
+ const LIBSIGNAL_RUNTIME_PATCH_ENABLED = parseEnvBool(process.env.BAILEYS_LIBSIGNAL_RUNTIME_PATCH_ENABLED, true);
25
+ /**
26
+ * Versão lógica do patch aplicado.
27
+ * @type {string}
28
+ */
29
+ const PATCH_VERSION = '2026-03-25';
30
+ /**
31
+ * Marker simbólico para identificar protótipos já patchados.
32
+ * @type {symbol}
33
+ */
34
+ const PATCH_MARKER = Symbol.for('omnizap.libsignal.runtimePatch');
35
+ /**
36
+ * Máximo de sessões fechadas mantidas no SessionRecord.
37
+ * @type {number}
38
+ */
39
+ const CLOSED_SESSIONS_MAX = 40;
40
+
41
+ /**
42
+ * Garante aplicação única por processo.
43
+ * @type {boolean}
44
+ */
45
+ let patchAttempted = false;
46
+
47
+ /**
48
+ * Marca um alvo como patchado.
49
+ * @param {object} target
50
+ * @param {string} [value=PATCH_VERSION]
51
+ * @returns {void}
52
+ */
53
+ const markPatched = (target, value = PATCH_VERSION) => {
54
+ try {
55
+ Object.defineProperty(target, PATCH_MARKER, {
56
+ value,
57
+ writable: false,
58
+ enumerable: false,
59
+ configurable: false,
60
+ });
61
+ } catch {
62
+ // Ignora erros de marcação para não impactar inicialização.
63
+ }
64
+ };
65
+
66
+ /**
67
+ * Verifica se um alvo já recebeu patch runtime.
68
+ * @param {any} target
69
+ * @returns {boolean}
70
+ */
71
+ const isPatched = (target) => Boolean(target?.[PATCH_MARKER]);
72
+
73
+ /**
74
+ * Aplica patch no SessionRecord para reduzir ruído de sessões antigas.
75
+ * @returns {boolean} `true` quando o patch foi aplicado nesta execução.
76
+ */
77
+ const patchSessionRecordLogging = () => {
78
+ const SessionRecord = require('libsignal/src/session_record.js');
79
+ const prototype = SessionRecord?.prototype;
80
+ if (!prototype || isPatched(prototype)) return false;
81
+
82
+ prototype.closeSession = function patchedCloseSession(session) {
83
+ if (!session || !session.indexInfo) return;
84
+ if (this.isClosed(session)) return;
85
+ session.indexInfo.closed = Date.now();
86
+ };
87
+
88
+ prototype.openSession = function patchedOpenSession(session) {
89
+ if (!session || !session.indexInfo) return;
90
+ session.indexInfo.closed = -1;
91
+ };
92
+
93
+ prototype.removeOldSessions = function patchedRemoveOldSessions() {
94
+ while (Object.keys(this.sessions).length > CLOSED_SESSIONS_MAX) {
95
+ let oldestKey;
96
+ let oldestSession;
97
+ for (const [key, session] of Object.entries(this.sessions)) {
98
+ if (session.indexInfo.closed !== -1 && (!oldestSession || session.indexInfo.closed < oldestSession.indexInfo.closed)) {
99
+ oldestKey = key;
100
+ oldestSession = session;
101
+ }
102
+ }
103
+ if (oldestKey) {
104
+ delete this.sessions[oldestKey];
105
+ } else {
106
+ throw new Error('Corrupt sessions object');
107
+ }
108
+ }
109
+ };
110
+
111
+ markPatched(prototype);
112
+ return true;
113
+ };
114
+
115
+ /**
116
+ * Aplica patch no SessionCipher para reduzir ruído de decrypt em sessões inválidas.
117
+ * @returns {boolean} `true` quando o patch foi aplicado nesta execução.
118
+ */
119
+ const patchSessionCipherNoise = () => {
120
+ const SessionCipher = require('libsignal/src/session_cipher.js');
121
+ const errors = require('libsignal/src/errors.js');
122
+ const prototype = SessionCipher?.prototype;
123
+ if (!prototype || isPatched(prototype)) return false;
124
+
125
+ prototype.decryptWithSessions = async function patchedDecryptWithSessions(data, sessions) {
126
+ if (!Array.isArray(sessions) || sessions.length === 0) {
127
+ throw new errors.SessionError('No sessions available');
128
+ }
129
+
130
+ for (const session of sessions) {
131
+ try {
132
+ const plaintext = await this.doDecryptWhisperMessage(data, session);
133
+ if (session?.indexInfo) {
134
+ session.indexInfo.used = Date.now();
135
+ }
136
+ return {
137
+ session,
138
+ plaintext,
139
+ };
140
+ } catch {
141
+ // Suprime ruído individual por sessão (Bad MAC esperado em rotação de ratchet).
142
+ }
143
+ }
144
+
145
+ throw new errors.SessionError('No matching sessions found for message');
146
+ };
147
+
148
+ prototype.decryptWhisperMessage = async function patchedDecryptWhisperMessage(data) {
149
+ if (!Buffer.isBuffer(data)) {
150
+ throw new TypeError(`Expected Buffer instead of: ${data?.constructor?.name || typeof data}`);
151
+ }
152
+ return await this.queueJob(async () => {
153
+ const record = await this.getRecord();
154
+ if (!record) {
155
+ throw new errors.SessionError('No session record');
156
+ }
157
+ const result = await this.decryptWithSessions(data, record.getSessions());
158
+ const remoteIdentityKey = result?.session?.indexInfo?.remoteIdentityKey;
159
+ if (!(await this.storage.isTrustedIdentity(this.addr.id, remoteIdentityKey))) {
160
+ throw new errors.UntrustedIdentityKeyError(this.addr.id, remoteIdentityKey);
161
+ }
162
+ // Mantemos comportamento funcional original, apenas sem log verboso por mensagem.
163
+ await this.storeRecord(record);
164
+ return result.plaintext;
165
+ });
166
+ };
167
+
168
+ markPatched(prototype);
169
+ return true;
170
+ };
171
+
172
+ /**
173
+ * Aplica patches runtime do libsignal de forma idempotente.
174
+ * @returns {void}
175
+ */
176
+ export const applyLibsignalRuntimePatch = () => {
177
+ if (patchAttempted) return;
178
+ patchAttempted = true;
179
+
180
+ if (!LIBSIGNAL_RUNTIME_PATCH_ENABLED) {
181
+ logger.info('Patch runtime do libsignal desativado por variável de ambiente.', {
182
+ action: 'libsignal_runtime_patch_disabled',
183
+ });
184
+ return;
185
+ }
186
+
187
+ try {
188
+ const recordPatched = patchSessionRecordLogging();
189
+ const cipherPatched = patchSessionCipherNoise();
190
+
191
+ if (!recordPatched && !cipherPatched) {
192
+ logger.debug('Patch runtime do libsignal já aplicado previamente.', {
193
+ action: 'libsignal_runtime_patch_already_applied',
194
+ });
195
+ return;
196
+ }
197
+
198
+ logger.info('Patch runtime do libsignal aplicado para reduzir ruído de sessão.', {
199
+ action: 'libsignal_runtime_patch_applied',
200
+ version: PATCH_VERSION,
201
+ patchedSessionRecord: recordPatched,
202
+ patchedSessionCipher: cipherPatched,
203
+ });
204
+ } catch (error) {
205
+ logger.warn('Falha ao aplicar patch runtime do libsignal. Seguindo sem patch.', {
206
+ action: 'libsignal_runtime_patch_failed',
207
+ error: error?.message,
208
+ });
209
+ }
210
+ };