@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
package/.env.example CHANGED
@@ -105,6 +105,7 @@ BAILEYS_GROUP_METADATA_CACHE_CHECKPERIOD_SECONDS=60
105
105
  BAILEYS_SEND_RETRY_ATTEMPTS=2
106
106
  BAILEYS_SEND_RETRY_BASE_DELAY_MS=600
107
107
  BAILEYS_SEND_MEDIA_UPLOAD_TIMEOUT_MS=0
108
+ BAILEYS_SEND_PREFER_PN_FOR_LID=true
108
109
  BAILEYS_VERSION=
109
110
  BAILEYS_FETCH_LATEST_VERSION=false
110
111
  BAILEYS_LOGGER_MODE=child
@@ -128,11 +129,13 @@ BAILEYS_AUTH_SESSION_ID=default
128
129
  BAILEYS_SESSION_IDS=default
129
130
  BAILEYS_PRIMARY_SESSION_ID=default
130
131
  BAILEYS_SESSION_WEIGHTS=default=1
132
+ BAILEYS_AUTH_KEYS_CACHE_ENABLED=true
131
133
  BAILEYS_AUTH_BOOTSTRAP_FROM_FILES=true
132
134
  BAILEYS_SINGLE_WRITER_LOCK_ENABLED=true
133
135
  BAILEYS_SINGLE_WRITER_LOCK_NAME=
134
136
  BAILEYS_SINGLE_WRITER_LOCK_TIMEOUT_SECONDS=2
135
137
  BAILEYS_SINGLE_WRITER_LOCK_RETRY_DELAY_MS=15000
138
+ BAILEYS_LIBSIGNAL_RUNTIME_PATCH_ENABLED=true
136
139
  GROUP_OWNER_ENFORCEMENT_MODE=off
137
140
  GROUP_OWNER_LEASE_MS=120000
138
141
  GROUP_OWNER_HEARTBEAT_MS=30000
@@ -322,6 +325,7 @@ WEB_USER_PASSWORD_RECOVERY_HASH_SECRET=
322
325
  WEB_URL=https://omnizap.shop
323
326
  WEB_VISITOR_COOKIE_TTL_SECONDS=31536000
324
327
  WHATSAPP_COMMAND_REQUIRES_GOOGLE_LOGIN=true
328
+ WHATSAPP_ALLOW_SELF_COMMANDS_ON_APPEND=true
325
329
  WHATSAPP_GOOGLE_LINK_CHECK_CACHE_TTL_MS=60000
326
330
  WHATSAPP_LOGIN_LINK_TTL_SECONDS=900
327
331
  WHATSAPP_LOGIN_REQUIRE_SIGNATURE=true
@@ -905,6 +909,26 @@ WIKI_SYNC_SOURCE_DIR=./docs/wiki
905
909
  WIKI_SYNC_TMP_DIR=/tmp/omnizap-wiki-sync
906
910
  STACK_NAME=omnizap
907
911
 
912
+ # ==============================
913
+ # PAYMENTS (STRIPE CHECKOUT + WEBHOOK)
914
+ # ==============================
915
+ STRIPE_PAYMENTS_ENABLED=true
916
+ PAYMENTS_API_BASE_PATH=/api/payments
917
+ PAYMENTS_WEB_PATH=/pagamentos
918
+ STRIPE_SECRET_KEY=
919
+ STRIPE_WEBHOOK_SECRET=
920
+ STRIPE_PRICE_ID=
921
+ STRIPE_CHECKOUT_MODE=subscription
922
+ STRIPE_PLAN_NAME=Plano Premium
923
+ STRIPE_PLAN_PRICE_LABEL=Assinatura recorrente
924
+ STRIPE_CHECKOUT_SUCCESS_URL=https://omnizap.shop/pagamentos/sucesso?session_id={CHECKOUT_SESSION_ID}
925
+ STRIPE_CHECKOUT_CANCEL_URL=https://omnizap.shop/pagamentos/cancelado
926
+ STRIPE_API_BASE_URL=https://api.stripe.com/v1
927
+ STRIPE_API_TIMEOUT_MS=10000
928
+ STRIPE_ALLOW_PROMOTION_CODES=true
929
+ STRIPE_WEBHOOK_TOLERANCE_SECONDS=300
930
+ STRIPE_AUTO_REVOKE_ON_CANCELLATION=false
931
+
908
932
  # ==============================
909
933
  # EMAIL AUTOMATION (SMTP/OUTBOX)
910
934
  # ==============================
@@ -1,3 +1,7 @@
1
+ /**
2
+ * Barrel de configuração da aplicação.
3
+ * Reexporta utilitários de Baileys, grupos, identidade admin, logger e sessão.
4
+ */
1
5
  export * from '../configParts/baileysConfig.js';
2
6
  export * from '../configParts/groupUtils.js';
3
7
  export * from '../configParts/adminIdentity.js';
@@ -2,8 +2,17 @@ import { encodeJid, getJidUser, isSameJidUser, normalizeJid } from './baileysCon
2
2
  import { extractUserIdInfo, resolveUserId, resolveUserIdCached } from './baileysConfig.js';
3
3
  import { normalizePhoneDigits, resolveAdminIdentityRawFromEnv, resolveAdminPhoneFromEnv } from '../../utils/whatsapp/contactEnv.js';
4
4
 
5
+ /**
6
+ * Retorna o valor bruto configurado para identidade de admin.
7
+ * @returns {string}
8
+ */
5
9
  export const getAdminRawValue = () => resolveAdminIdentityRawFromEnv();
6
10
 
11
+ /**
12
+ * Resolve o JID do administrador com base no valor de ambiente.
13
+ * Aceita JID completo ou telefone numérico.
14
+ * @returns {string|null}
15
+ */
7
16
  export const getAdminJid = () => {
8
17
  const raw = getAdminRawValue();
9
18
  if (!raw) return null;
@@ -24,6 +33,11 @@ export const getAdminJid = () => {
24
33
  return normalizedResolved || candidate;
25
34
  };
26
35
 
36
+ /**
37
+ * Resolve o telefone do administrador.
38
+ * Prioriza `ADMIN_PHONE` explícito e faz fallback para JID/identidade.
39
+ * @returns {string|null}
40
+ */
27
41
  export const getAdminPhone = () => {
28
42
  const explicitAdminPhone = resolveAdminPhoneFromEnv({ fallback: '' });
29
43
  if (explicitAdminPhone) return explicitAdminPhone;
@@ -38,6 +52,10 @@ export const getAdminPhone = () => {
38
52
  return digits || null;
39
53
  };
40
54
 
55
+ /**
56
+ * Resolve o JID do admin consultando reconciliação LID/JID quando disponível.
57
+ * @returns {Promise<string|null>}
58
+ */
41
59
  export const resolveAdminJid = async () => {
42
60
  const cached = getAdminJid();
43
61
  if (!cached) return null;
@@ -50,6 +68,11 @@ export const resolveAdminJid = async () => {
50
68
  }
51
69
  };
52
70
 
71
+ /**
72
+ * Verifica se um JID de remetente corresponde ao administrador.
73
+ * @param {string|null|undefined} senderJid
74
+ * @returns {boolean}
75
+ */
53
76
  export const isAdminSender = (senderJid) => {
54
77
  const adminJid = getAdminJid();
55
78
  if (!adminJid || !senderJid) return false;
@@ -60,6 +83,12 @@ export const isAdminSender = (senderJid) => {
60
83
  return isSameJidUser(normalizedSender, adminJid) || normalizedSender === adminJid;
61
84
  };
62
85
 
86
+ /**
87
+ * Verifica se a identidade do remetente corresponde ao administrador.
88
+ * Considera candidatos `jid`, `lid`, `participantAlt` e resolução assíncrona.
89
+ * @param {unknown} senderIdentity
90
+ * @returns {Promise<boolean>}
91
+ */
63
92
  export const isAdminSenderAsync = async (senderIdentity) => {
64
93
  const senderInfo = extractUserIdInfo(senderIdentity);
65
94
  if (!senderInfo.raw && !senderInfo.jid && !senderInfo.lid && !senderInfo.participantAlt) return false;
@@ -78,15 +78,28 @@ import { getMultiSessionRuntimeConfig } from './sessionConfig.js';
78
78
  * }} SenderInfo
79
79
  */
80
80
 
81
+ /**
82
+ * Versão local de fallback do Baileys.
83
+ * @type {number[]}
84
+ */
81
85
  const DEFAULT_BAILEYS_VERSION = [7, 0, 0];
82
86
  const multiSessionRuntimeConfig = getMultiSessionRuntimeConfig();
83
87
  const PRIMARY_BAILEYS_SESSION_ID = String(multiSessionRuntimeConfig?.primarySessionId || process.env.BAILEYS_AUTH_SESSION_ID || 'default').trim() || 'default';
84
88
 
89
+ /**
90
+ * Normaliza ID de sessão com fallback para sessão primária.
91
+ * @param {string|null|undefined} sessionId
92
+ * @returns {string}
93
+ */
85
94
  const normalizeSessionId = (sessionId) => {
86
95
  const normalized = String(sessionId || '').trim();
87
96
  return normalized || PRIMARY_BAILEYS_SESSION_ID;
88
97
  };
89
98
 
99
+ /**
100
+ * Mapa de sockets ativos por sessão.
101
+ * @type {Map<string, BaileysSocket>}
102
+ */
90
103
  const sessionSocketMap = new Map();
91
104
  let activeSocket = null;
92
105
 
@@ -265,6 +278,10 @@ export const WHATSAPP_USER_JID_SERVERS = new Set(['s.whatsapp.net', 'c.us', 'hos
265
278
  */
266
279
  export const LID_USER_JID_SERVERS = new Set(['lid', 'hosted.lid']);
267
280
 
281
+ /**
282
+ * Decode de JID com cache simples do último valor.
283
+ * @type {(jid: string) => ({user?: string, server?: string, domainType?: number, device?: number}|null)}
284
+ */
268
285
  const decodeJidParts = (() => {
269
286
  let lastJid = null;
270
287
  let lastDecoded = null;
@@ -341,10 +358,20 @@ export const MEDIA_TYPE_MAPPING = {
341
358
  */
342
359
  export const BINARY_MEDIA_TYPES = new Set(['image', 'video', 'videoNote', 'audio', 'voice', 'document', 'sticker']);
343
360
 
361
+ /**
362
+ * Aplica normalização nativa do Baileys no payload de mensagem.
363
+ * @param {Record<string, any>} message
364
+ * @returns {Record<string, any>}
365
+ */
344
366
  const normalizeMessage = (message) => normalizeMessageContent(message) || message;
345
367
 
346
368
  const MESSAGE_CONTENT_WRAPPER_KEYS = ['ephemeralMessage', 'viewOnceMessage', 'viewOnceMessageV2', 'viewOnceMessageV2Extension', 'deviceSentMessage', 'documentWithCaptionMessage', 'botInvokeMessage', 'editedMessage', 'keepInChatMessage'];
347
369
 
370
+ /**
371
+ * Resolve wrapper de mensagem de um nó com chave única.
372
+ * @param {Record<string, any>} node
373
+ * @returns {Record<string, any>|null}
374
+ */
348
375
  const resolveSingleWrapperMessage = (node) => {
349
376
  if (!node || typeof node !== 'object') return null;
350
377
 
@@ -359,6 +386,12 @@ const resolveSingleWrapperMessage = (node) => {
359
386
  return null;
360
387
  };
361
388
 
389
+ /**
390
+ * Remove camadas de wrappers (`ephemeral`, `viewOnce`, etc.) até o payload real.
391
+ * @param {Record<string, any>} message
392
+ * @param {number} [maxDepth=8]
393
+ * @returns {Record<string, any>}
394
+ */
362
395
  const unwrapMessageContent = (message, maxDepth = 8) => {
363
396
  let current = normalizeMessage(message);
364
397
  const visited = new Set();
@@ -392,6 +425,11 @@ const unwrapMessageContent = (message, maxDepth = 8) => {
392
425
  return current || message;
393
426
  };
394
427
 
428
+ /**
429
+ * Verifica se uma mediaKey está presente e não vazia.
430
+ * @param {unknown} mediaKey
431
+ * @returns {boolean}
432
+ */
395
433
  const hasNonEmptyMediaKey = (mediaKey) => {
396
434
  if (!mediaKey) return false;
397
435
 
@@ -417,6 +455,15 @@ const hasNonEmptyMediaKey = (mediaKey) => {
417
455
  return Boolean(mediaKey);
418
456
  };
419
457
 
458
+ /**
459
+ * Constrói entrada padronizada de mídia detectada.
460
+ * @param {string} mediaType
461
+ * @param {string} messageKey
462
+ * @param {Record<string, any>} value
463
+ * @param {boolean} isQuoted
464
+ * @param {Partial<MediaEntry>} [overrides={}]
465
+ * @returns {MediaEntry}
466
+ */
420
467
  const buildMediaEntry = (mediaType, messageKey, value, isQuoted, overrides = {}) => ({
421
468
  mediaType,
422
469
  mediaKey: value,
@@ -434,6 +481,12 @@ const buildMediaEntry = (mediaType, messageKey, value, isQuoted, overrides = {})
434
481
  ...overrides,
435
482
  });
436
483
 
484
+ /**
485
+ * Coleta mídias do corpo principal e, opcionalmente, de mensagem citada.
486
+ * @param {BaileysMessage|{message?: Record<string, any>}|Record<string, any>} message
487
+ * @param {{includeQuoted?: boolean}} [options]
488
+ * @returns {MediaEntry[]}
489
+ */
437
490
  const collectMediaFromMessage = (message, { includeQuoted = true } = {}) => {
438
491
  if (!message || !message.message) {
439
492
  return [];
@@ -452,6 +505,12 @@ const collectMediaFromMessage = (message, { includeQuoted = true } = {}) => {
452
505
  return allMedia;
453
506
  };
454
507
 
508
+ /**
509
+ * Filtra lista de mídia por tipo binário e inclusão de tipos desconhecidos.
510
+ * @param {MediaEntry[]} media
511
+ * @param {{includeAllTypes?: boolean, includeUnknown?: boolean}} [options]
512
+ * @returns {MediaEntry[]}
513
+ */
455
514
  const filterMedia = (media, { includeAllTypes = false, includeUnknown = false } = {}) => {
456
515
  let filtered = media;
457
516
 
@@ -466,6 +525,11 @@ const filterMedia = (media, { includeAllTypes = false, includeUnknown = false }
466
525
  return filtered;
467
526
  };
468
527
 
528
+ /**
529
+ * Procura o campo `contextInfo.expiration` de forma recursiva no payload.
530
+ * @param {Record<string, any>} root
531
+ * @returns {number|null}
532
+ */
469
533
  const findExpiration = (root) => {
470
534
  if (!root || typeof root !== 'object') return null;
471
535
 
@@ -491,6 +555,11 @@ const findExpiration = (root) => {
491
555
  return null;
492
556
  };
493
557
 
558
+ /**
559
+ * Resolve extensão padrão por tipo de mídia.
560
+ * @param {string} type
561
+ * @returns {string}
562
+ */
494
563
  const getMediaExtension = (type) => {
495
564
  if (type === 'image') return 'jpeg';
496
565
  if (type === 'video') return 'mp4';
@@ -498,6 +567,11 @@ const getMediaExtension = (type) => {
498
567
  return 'bin';
499
568
  };
500
569
 
570
+ /**
571
+ * Detecta erros de decrypt inválido (OpenSSL).
572
+ * @param {any} error
573
+ * @returns {boolean}
574
+ */
501
575
  const isBadDecryptError = (error) => {
502
576
  if (!error || typeof error !== 'object') return false;
503
577
  if (error.code === 'ERR_OSSL_BAD_DECRYPT') return true;
@@ -1189,6 +1263,10 @@ const authReverseLidCache = new Map();
1189
1263
 
1190
1264
  let backfillPromise = null;
1191
1265
 
1266
+ /**
1267
+ * Atualiza métrica de profundidade da fila `lid_map`.
1268
+ * @returns {void}
1269
+ */
1192
1270
  const updateLidQueueMetric = () => {
1193
1271
  setQueueDepth('lid_map', lidWriteBuffer.size);
1194
1272
  };
@@ -1199,20 +1277,40 @@ const updateLidQueueMetric = () => {
1199
1277
  */
1200
1278
  const now = () => __timeNowMs();
1201
1279
 
1280
+ /**
1281
+ * Normaliza um identificador LID.
1282
+ * @param {string|null|undefined} lid
1283
+ * @returns {string|null}
1284
+ */
1202
1285
  const normalizeLid = (lid) => {
1203
1286
  if (!lid || !isLidJid(lid)) return null;
1204
1287
  const normalized = normalizeJid(lid);
1205
1288
  return normalized || null;
1206
1289
  };
1207
1290
 
1291
+ /**
1292
+ * Normaliza JID de usuário WhatsApp.
1293
+ * @param {string|null|undefined} jid
1294
+ * @returns {string|null}
1295
+ */
1208
1296
  const normalizeWhatsAppJid = (jid) => {
1209
1297
  if (!jid || !isWhatsAppJid(jid)) return null;
1210
1298
  const normalized = normalizeJid(jid);
1211
1299
  return normalized || null;
1212
1300
  };
1213
1301
 
1302
+ /**
1303
+ * Mantém apenas dígitos de um valor.
1304
+ * @param {unknown} value
1305
+ * @returns {string}
1306
+ */
1214
1307
  const toDigits = (value) => String(value || '').replace(/\D+/g, '');
1215
1308
 
1309
+ /**
1310
+ * Extrai telefone (dígitos) de payload reverso LID.
1311
+ * @param {unknown} content
1312
+ * @returns {string}
1313
+ */
1216
1314
  const parseReverseMappingPhoneDigits = (content) => {
1217
1315
  const raw = String(content || '').trim();
1218
1316
  if (!raw) return '';
@@ -1228,6 +1326,11 @@ const parseReverseMappingPhoneDigits = (content) => {
1228
1326
  return digits.length >= 10 && digits.length <= 15 ? digits : '';
1229
1327
  };
1230
1328
 
1329
+ /**
1330
+ * Resolve JID a partir de LID consultando auth state (MySQL/arquivos).
1331
+ * @param {string} lid
1332
+ * @returns {Promise<string|null>}
1333
+ */
1231
1334
  const resolveAuthStoreJidByLid = async (lid) => {
1232
1335
  const normalizedLid = normalizeLid(lid);
1233
1336
  if (!normalizedLid) return null;
@@ -1519,6 +1622,11 @@ const resolveIdentityCandidates = ({ lid, jid, participantAlt } = {}) => {
1519
1622
  };
1520
1623
  };
1521
1624
 
1625
+ /**
1626
+ * Monta SQL de upsert para lote do `lid_map`.
1627
+ * @param {number} rows
1628
+ * @returns {string}
1629
+ */
1522
1630
  const buildLidUpsertSql = (rows) => {
1523
1631
  const placeholders = buildRowPlaceholders(rows, '(?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, ?)');
1524
1632
  return `
@@ -1768,6 +1876,10 @@ export const reconcileLidToJid = async ({ lid, jid, source = 'map' } = {}) => {
1768
1876
  return { updated };
1769
1877
  };
1770
1878
 
1879
+ /**
1880
+ * Executa flush em lote do buffer de atualizações LID->JID.
1881
+ * @returns {Promise<void>}
1882
+ */
1771
1883
  const flushLidQueueCore = async () => {
1772
1884
  if (lidWriteBuffer.size === 0) return;
1773
1885
  const entries = Array.from(lidWriteBuffer.values());
@@ -1861,6 +1973,10 @@ export const extractUserIdInfo = (value) => {
1861
1973
  };
1862
1974
  }
1863
1975
 
1976
+ /**
1977
+ * @param {unknown} entry
1978
+ * @returns {string|null}
1979
+ */
1864
1980
  const readJid = (entry) => (typeof entry === 'string' ? normalizeJid(entry) || null : null);
1865
1981
 
1866
1982
  const participantAlt = readJid(value.participantAlt);