@omnizap-system/omnizap 2.6.1 → 2.6.2

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 (156) hide show
  1. package/.env.example +54 -9
  2. package/.github/workflows/ci.yml +3 -3
  3. package/.github/workflows/security-runner-hardening.yml +1 -1
  4. package/.github/workflows/security-zap-full-scan.yml +1 -0
  5. package/app/config/index.js +2 -0
  6. package/app/configParts/adminIdentity.js +5 -5
  7. package/app/configParts/baileysConfig.js +226 -55
  8. package/app/configParts/groupUtils.js +5 -0
  9. package/app/configParts/messagePersistenceService.js +143 -3
  10. package/app/configParts/sessionConfig.js +157 -0
  11. package/app/connection/baileysCompatibility.test.js +1 -1
  12. package/app/connection/groupOwnerWriteStateResolver.js +109 -0
  13. package/app/connection/socketController.js +625 -124
  14. package/app/connection/socketController.multiSession.test.js +108 -0
  15. package/app/controllers/messageController.js +1 -1
  16. package/app/controllers/messagePipeline/commandMiddleware.js +12 -10
  17. package/app/controllers/messagePipeline/conversationMiddleware.js +2 -1
  18. package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +104 -0
  19. package/app/controllers/messagePipeline/preProcessingMiddlewares.js +80 -2
  20. package/app/controllers/messageProcessingPipeline.js +88 -9
  21. package/app/controllers/messageProcessingPipeline.test.js +200 -0
  22. package/app/modules/adminModule/AGENT.md +1 -1
  23. package/app/modules/adminModule/commandConfig.json +3318 -1347
  24. package/app/modules/adminModule/groupCommandHandlers.js +856 -14
  25. package/app/modules/adminModule/groupCommandHandlers.test.js +375 -9
  26. package/app/modules/adminModule/groupWarningRepository.js +152 -0
  27. package/app/modules/aiModule/AGENT.md +47 -30
  28. package/app/modules/aiModule/aiConfigRuntime.js +1 -0
  29. package/app/modules/aiModule/catCommand.js +132 -25
  30. package/app/modules/aiModule/commandConfig.json +114 -28
  31. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +54 -6
  32. package/app/modules/gameModule/AGENT.md +1 -1
  33. package/app/modules/gameModule/commandConfig.json +29 -0
  34. package/app/modules/menuModule/AGENT.md +1 -1
  35. package/app/modules/menuModule/commandConfig.json +45 -10
  36. package/app/modules/menuModule/menuCatalogService.js +190 -0
  37. package/app/modules/menuModule/menuCommandUsageRepository.js +109 -0
  38. package/app/modules/menuModule/menuDynamicService.js +511 -0
  39. package/app/modules/menuModule/menuDynamicService.test.js +141 -0
  40. package/app/modules/menuModule/menus.js +36 -5
  41. package/app/modules/playModule/AGENT.md +10 -5
  42. package/app/modules/playModule/commandConfig.json +74 -16
  43. package/app/modules/playModule/playCommandConstants.js +13 -7
  44. package/app/modules/playModule/playCommandCore.js +4 -6
  45. package/app/modules/playModule/{playCommandYtDlpClient.js → playCommandMediaClient.js} +684 -332
  46. package/app/modules/playModule/playConfigRuntime.js +5 -6
  47. package/app/modules/playModule/playModuleCriticalFlows.test.js +44 -59
  48. package/app/modules/quoteModule/AGENT.md +1 -1
  49. package/app/modules/quoteModule/commandConfig.json +29 -0
  50. package/app/modules/rpgPokemonModule/AGENT.md +1 -1
  51. package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
  52. package/app/modules/statsModule/AGENT.md +1 -1
  53. package/app/modules/statsModule/commandConfig.json +58 -0
  54. package/app/modules/stickerModule/AGENT.md +1 -1
  55. package/app/modules/stickerModule/commandConfig.json +145 -0
  56. package/app/modules/stickerPackModule/AGENT.md +1 -1
  57. package/app/modules/stickerPackModule/autoPackCollectorService.js +5 -1
  58. package/app/modules/stickerPackModule/commandConfig.json +29 -0
  59. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +1 -1
  60. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +78 -57
  61. package/app/modules/stickerPackModule/stickerPackService.js +13 -6
  62. package/app/modules/systemMetricsModule/AGENT.md +1 -1
  63. package/app/modules/systemMetricsModule/commandConfig.json +29 -0
  64. package/app/modules/tiktokModule/AGENT.md +1 -1
  65. package/app/modules/tiktokModule/commandConfig.json +29 -0
  66. package/app/modules/userModule/AGENT.md +1 -1
  67. package/app/modules/userModule/commandConfig.json +29 -0
  68. package/app/modules/waifuPicsModule/AGENT.md +57 -27
  69. package/app/modules/waifuPicsModule/commandConfig.json +87 -0
  70. package/app/observability/metrics.js +136 -0
  71. package/app/services/ai/commandConfigEnrichmentService.js +229 -47
  72. package/app/services/ai/geminiService.js +131 -7
  73. package/app/services/ai/geminiService.test.js +59 -2
  74. package/app/services/ai/moduleAiHelpCoreService.js +33 -4
  75. package/app/services/group/groupMetadataService.js +24 -1
  76. package/app/services/infra/dbWriteQueue.js +51 -21
  77. package/app/services/messaging/newsBroadcastService.js +843 -27
  78. package/app/services/multiSession/assignmentBalancerService.js +457 -0
  79. package/app/services/multiSession/groupOwnershipRepository.js +381 -0
  80. package/app/services/multiSession/groupOwnershipService.js +890 -0
  81. package/app/services/multiSession/groupOwnershipService.test.js +309 -0
  82. package/app/services/multiSession/sessionRegistryService.js +293 -0
  83. package/app/store/aiPromptStore.js +36 -19
  84. package/app/store/groupConfigStore.js +41 -5
  85. package/app/store/premiumUserStore.js +21 -7
  86. package/app/utils/antiLink/antiLinkModule.js +352 -16
  87. package/app/workers/aiHelperContinuousLearningWorker.js +512 -0
  88. package/database/index.js +6 -0
  89. package/database/migrations/20260307_d0_hardening_down.sql +1 -1
  90. package/database/migrations/20260314_d7_canonical_sender_down.sql +1 -1
  91. package/database/migrations/20260406_d30_security_analytics_down.sql +1 -1
  92. package/database/migrations/20260411_d35_group_community_metadata_down.sql +59 -0
  93. package/database/migrations/20260411_d35_group_community_metadata_up.sql +62 -0
  94. package/database/migrations/20260412_d36_system_config_tables_down.sql +32 -0
  95. package/database/migrations/20260412_d36_system_config_tables_up.sql +66 -0
  96. package/database/migrations/20260413_d37_group_user_warnings_down.sql +11 -0
  97. package/database/migrations/20260413_d37_group_user_warnings_up.sql +24 -0
  98. package/database/migrations/20260414_d38_multi_session_foundation_down.sql +72 -0
  99. package/database/migrations/20260414_d38_multi_session_foundation_up.sql +125 -0
  100. package/database/migrations/20260414_d39_multi_session_cutover_down.sql +103 -0
  101. package/database/migrations/20260414_d39_multi_session_cutover_up.sql +83 -0
  102. package/database/schema.sql +102 -1
  103. package/docker-compose.yml +4 -1
  104. package/docs/compliance/acceptable-use-policy-2026-03-07.md +1 -1
  105. package/docs/compliance/privacy-policy-2026-03-07.md +2 -2
  106. package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
  107. package/docs/security/network-hardening-runbook-2026-03-07.md +53 -0
  108. package/docs/security/omnizap-static-security-headers.conf +25 -0
  109. package/ecosystem.prod.config.cjs +31 -11
  110. package/index.js +52 -18
  111. package/observability/alert-rules.yml +20 -0
  112. package/observability/grafana/dashboards/omnizap-system-admin.json +229 -0
  113. package/observability/mysql-setup.sql +4 -4
  114. package/observability/system-admin-observability.md +26 -0
  115. package/package.json +12 -5
  116. package/public/comandos/commands-catalog.json +2253 -78
  117. package/public/js/apps/commandsReactApp.js +267 -87
  118. package/public/js/apps/createPackApp.js +3 -3
  119. package/public/js/apps/stickersApp.js +255 -103
  120. package/public/js/apps/termsReactApp.js +57 -8
  121. package/public/js/apps/userPasswordResetReactApp.js +406 -0
  122. package/public/js/apps/userReactApp.js +96 -47
  123. package/public/js/apps/userSystemAdmReactApp.js +1506 -0
  124. package/public/pages/politica-de-privacidade.html +1 -1
  125. package/public/pages/stickers.html +5 -5
  126. package/public/pages/termos-de-uso-texto-integral.html +1 -1
  127. package/public/pages/termos-de-uso.html +1 -1
  128. package/public/pages/user-password-reset.html +3 -4
  129. package/public/pages/user-systemadm.html +8 -462
  130. package/public/pages/user.html +1 -1
  131. package/scripts/clear-whatsapp-session.sh +123 -0
  132. package/scripts/core-ai-mode.mjs +163 -0
  133. package/scripts/deploy.sh +10 -0
  134. package/scripts/enrich-command-config-ux-openai.mjs +492 -0
  135. package/scripts/generate-commands-catalog.mjs +155 -0
  136. package/scripts/new-whatsapp-session.sh +317 -0
  137. package/scripts/security-web-surface-check.mjs +218 -0
  138. package/server/controllers/admin/adminPanelHandlers.js +253 -3
  139. package/server/controllers/admin/systemAdminController.js +267 -0
  140. package/server/controllers/sticker/stickerCatalogController.js +9 -23
  141. package/server/controllers/system/contactController.js +9 -17
  142. package/server/controllers/system/stickerCatalogSystemContext.js +27 -6
  143. package/server/controllers/system/systemController.js +254 -1
  144. package/server/controllers/userController.js +6 -0
  145. package/server/email/emailTemplateService.js +3 -2
  146. package/server/http/httpServer.js +8 -4
  147. package/server/middleware/securityHeaders.js +20 -1
  148. package/server/routes/admin/systemAdminRouter.js +6 -0
  149. package/server/routes/indexRouter.js +30 -6
  150. package/server/routes/observability/grafanaProxyRouter.js +254 -0
  151. package/server/routes/static/staticPageRouter.js +27 -1
  152. package/server/utils/publicContact.js +31 -0
  153. package/utils/whatsapp/contactEnv.js +39 -0
  154. package/vite.config.mjs +2 -1
  155. package/app/modules/playModule/local/installYtDlp.js +0 -25
  156. package/app/modules/playModule/local/ytDlpInstaller.js +0 -28
@@ -2,6 +2,7 @@ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } fro
2
2
  import { baileysConnectionLogger as logger } from './loggerConfig.js';
3
3
  import { queueMessageInsert } from '../services/infra/dbWriteQueue.js';
4
4
  import { parseEnvBool, parseEnvInt, normalizeJid, isGroupJid, isStatusJid, isBroadcastJid, isNewsletterJid, normalizeWAPresence } from './baileysConfig.js';
5
+ import { getOwner as getGroupOwner, tryAcquire as tryAcquireGroupOwner } from '../services/multiSession/groupOwnershipService.js';
5
6
 
6
7
  const BAILEYS_SEND_RETRY_ATTEMPTS = parseEnvInt(process.env.BAILEYS_SEND_RETRY_ATTEMPTS, 2, 1, 5);
7
8
  const BAILEYS_SEND_RETRY_BASE_DELAY_MS = parseEnvInt(process.env.BAILEYS_SEND_RETRY_BASE_DELAY_MS, 600, 100, 10_000);
@@ -11,11 +12,102 @@ const BAILEYS_REPLY_PRESENCE_SUBSCRIBE = parseEnvBool(process.env.BAILEYS_REPLY_
11
12
  const BAILEYS_REPLY_PRESENCE_DELAY_MS = parseEnvInt(process.env.BAILEYS_REPLY_PRESENCE_DELAY_MS, 280, 0, 3_000);
12
13
  const BAILEYS_REPLY_PRESENCE_BEFORE = normalizeWAPresence(process.env.BAILEYS_REPLY_PRESENCE_BEFORE, 'composing');
13
14
  const BAILEYS_REPLY_PRESENCE_AFTER = normalizeWAPresence(process.env.BAILEYS_REPLY_PRESENCE_AFTER, 'paused');
15
+ const GROUP_WRITE_PERMISSION_CACHE_TTL_MS = parseEnvInt(process.env.GROUP_OWNER_WRITE_CACHE_TTL_MS, 8_000, 1_000, 60_000);
14
16
 
15
17
  const isPlainObject = (value) => Object.prototype.toString.call(value) === '[object Object]';
16
18
 
17
19
  const ANY_MESSAGE_CONTENT_PRIMARY_KEYS = new Set(['text', 'image', 'video', 'audio', 'sticker', 'stickerPack', 'stickerPackMessage', 'document', 'event', 'poll', 'contacts', 'location', 'react', 'buttonReply', 'groupInvite', 'listReply', 'pin', 'product', 'sharePhoneNumber', 'requestPhoneNumber', 'forward', 'delete', 'disappearingMessagesInChat', 'limitSharing']);
18
20
  const PRESENCE_NON_REPLY_CONTENT_KEYS = new Set(['react', 'delete', 'pin', 'disappearingMessagesInChat']);
21
+ const groupWritePermissionCache = new Map();
22
+
23
+ const normalizeSessionId = (value) => {
24
+ const normalized = String(value || '').trim();
25
+ return normalized || null;
26
+ };
27
+
28
+ const getCachedGroupWritePermission = (groupJid, sessionId) => {
29
+ const key = `${sessionId}:${groupJid}`;
30
+ const cached = groupWritePermissionCache.get(key);
31
+ if (!cached) return null;
32
+ if (cached.expiresAtMs <= __timeNowMs()) {
33
+ groupWritePermissionCache.delete(key);
34
+ return null;
35
+ }
36
+ return cached;
37
+ };
38
+
39
+ const setCachedGroupWritePermission = (groupJid, sessionId, allowed, ownerSessionId = null) => {
40
+ const key = `${sessionId}:${groupJid}`;
41
+ groupWritePermissionCache.set(key, {
42
+ allowed: Boolean(allowed),
43
+ ownerSessionId: normalizeSessionId(ownerSessionId),
44
+ expiresAtMs: __timeNowMs() + GROUP_WRITE_PERMISSION_CACHE_TTL_MS,
45
+ });
46
+ };
47
+
48
+ const resolveGroupWritePermission = async (groupJid, sessionId) => {
49
+ if (!isGroupJid(groupJid) || !sessionId) {
50
+ return {
51
+ allowed: true,
52
+ ownerSessionId: null,
53
+ reason: 'not_group_or_missing_session',
54
+ };
55
+ }
56
+
57
+ const cached = getCachedGroupWritePermission(groupJid, sessionId);
58
+ if (cached) {
59
+ return {
60
+ allowed: cached.allowed,
61
+ ownerSessionId: cached.ownerSessionId,
62
+ reason: 'cache_hit',
63
+ };
64
+ }
65
+
66
+ try {
67
+ const ownerState = await getGroupOwner(groupJid);
68
+ let ownerSessionId = normalizeSessionId(ownerState?.ownerSessionId);
69
+ let allowed = false;
70
+ let reason = 'owned_by_other';
71
+
72
+ if (!ownerSessionId) {
73
+ const claimOutcome = await tryAcquireGroupOwner({
74
+ groupJid,
75
+ sessionId,
76
+ reason: 'send_store_claim',
77
+ changedBy: sessionId,
78
+ metadata: {
79
+ source: 'message_persistence_service',
80
+ gate: 'group_write',
81
+ },
82
+ });
83
+ ownerSessionId = normalizeSessionId(claimOutcome?.owner?.ownerSessionId);
84
+ allowed = Boolean(claimOutcome?.acquired && ownerSessionId === sessionId);
85
+ reason = claimOutcome?.reason || 'claim_attempt';
86
+ } else {
87
+ allowed = ownerSessionId === sessionId;
88
+ reason = allowed ? 'owner_match' : 'owned_by_other';
89
+ }
90
+
91
+ setCachedGroupWritePermission(groupJid, sessionId, allowed, ownerSessionId);
92
+ return {
93
+ allowed,
94
+ ownerSessionId,
95
+ reason,
96
+ };
97
+ } catch (error) {
98
+ logger.warn('Falha ao validar ownership para persistência de saída em grupo.', {
99
+ action: 'group_write_permission_resolution_failed',
100
+ groupJid,
101
+ sessionId,
102
+ error: error?.message,
103
+ });
104
+ return {
105
+ allowed: false,
106
+ ownerSessionId: null,
107
+ reason: 'resolution_failed',
108
+ };
109
+ }
110
+ };
19
111
 
20
112
  /**
21
113
  * Verifica se o payload se parece com AnyMessageContent do Baileys.
@@ -62,6 +154,8 @@ const normalizeSendOptions = (options) => {
62
154
  * @param {unknown} options
63
155
  * @returns {{
64
156
  * sendOptions: import('@whiskeysockets/baileys').MiscMessageGenerationOptions|undefined,
157
+ * sessionId: string | null,
158
+ * allowGroupWrite: boolean | undefined,
65
159
  * skipPresenceUpdate: boolean,
66
160
  * presenceBefore: import('@whiskeysockets/baileys').WAPresence,
67
161
  * presenceAfter: import('@whiskeysockets/baileys').WAPresence,
@@ -73,6 +167,8 @@ const resolveRuntimeSendOptions = (options) => {
73
167
  if (!isPlainObject(options)) {
74
168
  return {
75
169
  sendOptions: undefined,
170
+ sessionId: null,
171
+ allowGroupWrite: undefined,
76
172
  skipPresenceUpdate: false,
77
173
  presenceBefore: BAILEYS_REPLY_PRESENCE_BEFORE,
78
174
  presenceAfter: BAILEYS_REPLY_PRESENCE_AFTER,
@@ -81,10 +177,12 @@ const resolveRuntimeSendOptions = (options) => {
81
177
  };
82
178
  }
83
179
 
84
- const { skipPresenceUpdate, presenceBefore, presenceAfter, presenceDelayMs, presenceSubscribe, ...sendOptions } = options;
180
+ const { skipPresenceUpdate, presenceBefore, presenceAfter, presenceDelayMs, presenceSubscribe, sessionId, allowGroupWrite, ...sendOptions } = options;
85
181
  const normalizedDelay = parseEnvInt(presenceDelayMs, BAILEYS_REPLY_PRESENCE_DELAY_MS, 0, 3_000);
86
182
  return {
87
183
  sendOptions: Object.keys(sendOptions).length > 0 ? sendOptions : undefined,
184
+ sessionId: normalizeSessionId(sessionId),
185
+ allowGroupWrite: typeof allowGroupWrite === 'boolean' ? allowGroupWrite : undefined,
88
186
  skipPresenceUpdate: Boolean(skipPresenceUpdate),
89
187
  presenceBefore: normalizeWAPresence(presenceBefore, BAILEYS_REPLY_PRESENCE_BEFORE),
90
188
  presenceAfter: normalizeWAPresence(presenceAfter, BAILEYS_REPLY_PRESENCE_AFTER),
@@ -163,9 +261,11 @@ const resolveMessageTimestampMs = (msg) => {
163
261
  * Normaliza uma mensagem do Baileys para o formato persistido no banco.
164
262
  * @param {import('@whiskeysockets/baileys').WAMessage} msg - Mensagem recebida/enviada.
165
263
  * @param {string} [senderId] - ID do remetente (opcional).
264
+ * @param {string|null} [sessionId] - Sessão lógica para persistência.
166
265
  * @returns {Object} Objeto com dados prontos para persistencia.
167
266
  */
168
- export const buildMessageData = (msg, senderId) => ({
267
+ export const buildMessageData = (msg, senderId, sessionId = null) => ({
268
+ session_id: normalizeSessionId(sessionId),
169
269
  message_id: msg?.key?.id,
170
270
  chat_id: msg?.key?.remoteJid,
171
271
  sender_id: senderId || msg?.key?.participant || msg?.key?.remoteJid,
@@ -227,6 +327,40 @@ export async function sendAndStore(sock, jid, content, options) {
227
327
 
228
328
  const normalizedJid = normalizeJid(jid) || String(jid).trim();
229
329
  const runtimeOptions = resolveRuntimeSendOptions(options);
330
+ const runtimeSessionId = runtimeOptions.sessionId || normalizeSessionId(sock?.__omnizapSessionId);
331
+ let resolvedGroupWritePermission = null;
332
+
333
+ if (isGroupJid(normalizedJid)) {
334
+ if (runtimeOptions.allowGroupWrite === false) {
335
+ logger.debug('Envio para grupo ignorado por bloqueio explícito de escrita.', {
336
+ action: 'send_group_blocked_explicit',
337
+ groupJid: normalizedJid,
338
+ sessionId: runtimeSessionId,
339
+ });
340
+ return undefined;
341
+ }
342
+
343
+ if (runtimeOptions.allowGroupWrite === true) {
344
+ resolvedGroupWritePermission = {
345
+ allowed: true,
346
+ ownerSessionId: runtimeSessionId,
347
+ reason: 'explicit_allow',
348
+ };
349
+ } else {
350
+ resolvedGroupWritePermission = await resolveGroupWritePermission(normalizedJid, runtimeSessionId);
351
+ if (!resolvedGroupWritePermission.allowed) {
352
+ logger.info('Envio para grupo bloqueado por sessão não-owner.', {
353
+ action: 'send_group_blocked_non_owner',
354
+ groupJid: normalizedJid,
355
+ sessionId: runtimeSessionId,
356
+ ownerSessionId: resolvedGroupWritePermission.ownerSessionId,
357
+ reason: resolvedGroupWritePermission.reason,
358
+ });
359
+ return undefined;
360
+ }
361
+ }
362
+ }
363
+
230
364
  const normalizedOptions = normalizeSendOptions(runtimeOptions.sendOptions);
231
365
  const shouldSendPresence = shouldSendReplyPresence(normalizedJid, content, runtimeOptions);
232
366
 
@@ -292,7 +426,13 @@ export async function sendAndStore(sock, jid, content, options) {
292
426
  const senderId = sock?.user?.id || sent?.key?.participant;
293
427
  if (sent?.key?.id) {
294
428
  try {
295
- queueMessageInsert(buildMessageData(sent, senderId));
429
+ const messageData = buildMessageData(sent, senderId, runtimeSessionId);
430
+ const targetGroupJid = normalizeJid(messageData.chat_id || normalizedJid);
431
+ if (isGroupJid(targetGroupJid)) {
432
+ const allowGroupWrite = runtimeOptions.allowGroupWrite === true || resolvedGroupWritePermission?.allowed === true;
433
+ messageData.allow_group_write = allowGroupWrite;
434
+ }
435
+ queueMessageInsert(messageData);
296
436
  } catch (error) {
297
437
  logger.warn('Falha ao enfileirar mensagem enviada para persistencia.', {
298
438
  error: error.message,
@@ -0,0 +1,157 @@
1
+ const DEFAULT_SESSION_ID = 'default';
2
+ const SESSION_ID_MAX_LENGTH = 64;
3
+ const SESSION_ID_PATTERN = /^[a-zA-Z0-9:_-]+$/;
4
+ const OWNER_ENFORCEMENT_MODES = new Set(['off', 'shadow', 'enforce']);
5
+
6
+ const parseEnvBool = (value, fallback) => {
7
+ if (value === undefined || value === null || value === '') return fallback;
8
+ const normalized = String(value).trim().toLowerCase();
9
+ if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
10
+ if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
11
+ return fallback;
12
+ };
13
+
14
+ const parseEnvInt = (value, fallback, min, max) => {
15
+ const parsed = Number(value);
16
+ if (!Number.isFinite(parsed)) return fallback;
17
+ return Math.max(min, Math.min(max, Math.floor(parsed)));
18
+ };
19
+
20
+ const parseFlexibleEntries = (value) =>
21
+ String(value || '')
22
+ .split(/[,\n;]+/g)
23
+ .map((entry) => String(entry || '').trim())
24
+ .filter(Boolean);
25
+
26
+ const normalizeSessionId = (value) => String(value || '').trim();
27
+
28
+ const isValidSessionId = (value) => {
29
+ const normalized = normalizeSessionId(value);
30
+ if (!normalized) return false;
31
+ if (normalized.length > SESSION_ID_MAX_LENGTH) return false;
32
+ return SESSION_ID_PATTERN.test(normalized);
33
+ };
34
+
35
+ const parseSessionIds = ({ sessionIdsRaw = '', legacySessionIdRaw = '' } = {}) => {
36
+ const warnings = [];
37
+ const validSessionIds = [];
38
+ const seen = new Set();
39
+
40
+ const legacySessionId = normalizeSessionId(legacySessionIdRaw) || DEFAULT_SESSION_ID;
41
+ const requestedSessionIds = parseFlexibleEntries(sessionIdsRaw);
42
+ const sourceSessionIds = requestedSessionIds.length > 0 ? requestedSessionIds : [legacySessionId];
43
+
44
+ for (const candidate of sourceSessionIds) {
45
+ const sessionId = normalizeSessionId(candidate);
46
+ if (!isValidSessionId(sessionId)) {
47
+ warnings.push(`session_id invalido ignorado: "${candidate}"`);
48
+ continue;
49
+ }
50
+ if (seen.has(sessionId)) continue;
51
+ seen.add(sessionId);
52
+ validSessionIds.push(sessionId);
53
+ }
54
+
55
+ if (validSessionIds.length === 0) {
56
+ validSessionIds.push(DEFAULT_SESSION_ID);
57
+ warnings.push(`nenhum session_id valido encontrado; usando fallback "${DEFAULT_SESSION_ID}"`);
58
+ }
59
+
60
+ return {
61
+ sessionIds: validSessionIds,
62
+ warnings,
63
+ };
64
+ };
65
+
66
+ const parseSessionWeights = (rawValue, sessionIds, warnings) => {
67
+ const allowedSessions = new Set(sessionIds);
68
+ const weights = {};
69
+ for (const sessionId of sessionIds) {
70
+ weights[sessionId] = 1;
71
+ }
72
+
73
+ const entries = parseFlexibleEntries(rawValue);
74
+ if (entries.length === 0) return weights;
75
+
76
+ for (const entry of entries) {
77
+ const separator = entry.includes('=') ? '=' : entry.includes(':') ? ':' : '';
78
+ if (!separator) {
79
+ warnings.push(`peso de sessao invalido (faltando separador "=" ou ":"): "${entry}"`);
80
+ continue;
81
+ }
82
+
83
+ const [rawSessionId, rawWeight] = entry.split(separator, 2);
84
+ const sessionId = normalizeSessionId(rawSessionId);
85
+ if (!isValidSessionId(sessionId)) {
86
+ warnings.push(`peso ignorado para session_id invalido: "${rawSessionId}"`);
87
+ continue;
88
+ }
89
+ if (!allowedSessions.has(sessionId)) {
90
+ warnings.push(`peso ignorado para session_id nao listado em BAILEYS_SESSION_IDS: "${sessionId}"`);
91
+ continue;
92
+ }
93
+
94
+ const weight = parseEnvInt(rawWeight, Number.NaN, 1, 1000);
95
+ if (!Number.isFinite(weight)) {
96
+ warnings.push(`peso invalido para "${sessionId}": "${rawWeight}"`);
97
+ continue;
98
+ }
99
+ weights[sessionId] = weight;
100
+ }
101
+
102
+ return weights;
103
+ };
104
+
105
+ export const resolveMultiSessionRuntimeConfig = (env = process.env) => {
106
+ const warnings = [];
107
+ const legacySessionId = normalizeSessionId(env.BAILEYS_AUTH_SESSION_ID) || DEFAULT_SESSION_ID;
108
+ const { sessionIds, warnings: parseWarnings } = parseSessionIds({
109
+ sessionIdsRaw: env.BAILEYS_SESSION_IDS,
110
+ legacySessionIdRaw: legacySessionId,
111
+ });
112
+ warnings.push(...parseWarnings);
113
+
114
+ const requestedPrimary = normalizeSessionId(env.BAILEYS_PRIMARY_SESSION_ID);
115
+ let primarySessionId = requestedPrimary || sessionIds[0];
116
+
117
+ if (requestedPrimary && !isValidSessionId(requestedPrimary)) {
118
+ warnings.push(`BAILEYS_PRIMARY_SESSION_ID invalido: "${requestedPrimary}"`);
119
+ primarySessionId = sessionIds[0];
120
+ } else if (requestedPrimary && !sessionIds.includes(requestedPrimary)) {
121
+ warnings.push(`BAILEYS_PRIMARY_SESSION_ID fora da lista de sessoes: "${requestedPrimary}"`);
122
+ primarySessionId = sessionIds[0];
123
+ }
124
+
125
+ const ownerEnforcementModeRaw = String(env.GROUP_OWNER_ENFORCEMENT_MODE || 'off')
126
+ .trim()
127
+ .toLowerCase();
128
+ const ownerEnforcementMode = OWNER_ENFORCEMENT_MODES.has(ownerEnforcementModeRaw) ? ownerEnforcementModeRaw : 'off';
129
+ if (!OWNER_ENFORCEMENT_MODES.has(ownerEnforcementModeRaw)) {
130
+ warnings.push(`GROUP_OWNER_ENFORCEMENT_MODE invalido: "${ownerEnforcementModeRaw}"`);
131
+ }
132
+
133
+ const ownerLeaseMs = parseEnvInt(env.GROUP_OWNER_LEASE_MS, 120_000, 5_000, 15 * 60 * 1000);
134
+ let ownerHeartbeatMs = parseEnvInt(env.GROUP_OWNER_HEARTBEAT_MS, 30_000, 1_000, 5 * 60 * 1000);
135
+ if (ownerHeartbeatMs >= ownerLeaseMs) {
136
+ ownerHeartbeatMs = Math.max(1_000, Math.floor(ownerLeaseMs / 2));
137
+ warnings.push(`GROUP_OWNER_HEARTBEAT_MS ajustado automaticamente para ${ownerHeartbeatMs}ms (precisa ser menor que lease)`);
138
+ }
139
+
140
+ const balancerEnabled = parseEnvBool(env.GROUP_BALANCER_ENABLED, false);
141
+ const sessionWeights = parseSessionWeights(env.BAILEYS_SESSION_WEIGHTS, sessionIds, warnings);
142
+
143
+ return Object.freeze({
144
+ sessionIds: Object.freeze([...sessionIds]),
145
+ primarySessionId,
146
+ sessionWeights: Object.freeze({ ...sessionWeights }),
147
+ ownerEnforcementMode,
148
+ ownerLeaseMs,
149
+ ownerHeartbeatMs,
150
+ balancerEnabled,
151
+ warnings: Object.freeze([...warnings]),
152
+ });
153
+ };
154
+
155
+ export const multiSessionRuntimeConfig = resolveMultiSessionRuntimeConfig();
156
+
157
+ export const getMultiSessionRuntimeConfig = () => multiSessionRuntimeConfig;
@@ -6,7 +6,7 @@ import test from 'node:test';
6
6
 
7
7
  import { initAuthCreds, proto } from '@whiskeysockets/baileys';
8
8
 
9
- const PINNED_BAILEYS_REF = 'github:jlucaso1/Baileys#be89465e07afa871cf3f0e19cabfec9780db6be7';
9
+ const PINNED_BAILEYS_REF = 'github:jlucaso1/Baileys#feat-add-stickerpack-support';
10
10
 
11
11
  const require = createRequire(import.meta.url);
12
12
  const baileysPackageJsonPath = require.resolve('@whiskeysockets/baileys/package.json');
@@ -0,0 +1,109 @@
1
+ export const normalizeAssignmentVersion = (value) => {
2
+ const parsed = Number.parseInt(String(value ?? ''), 10);
3
+ if (!Number.isFinite(parsed) || parsed <= 0) return null;
4
+ return parsed;
5
+ };
6
+
7
+ export const createGroupOwnerWriteStateResolver = ({
8
+ buildCacheKeyImpl,
9
+ getOwnerImpl,
10
+ tryAcquireImpl,
11
+ cacheImpl,
12
+ isGroupJidImpl,
13
+ normalizeSessionIdImpl,
14
+ loggerImpl,
15
+ defaultAllowClaim = true,
16
+ } = {}) =>
17
+ async (
18
+ groupJid,
19
+ sessionId,
20
+ {
21
+ allowClaim = defaultAllowClaim,
22
+ bypassCache = false,
23
+ source = 'unknown',
24
+ expectedAssignmentVersion = null,
25
+ enforceFence = true,
26
+ } = {},
27
+ ) => {
28
+ const safeGroupJid = String(groupJid || '').trim();
29
+ const safeSessionId = normalizeSessionIdImpl(sessionId);
30
+ if (!safeGroupJid || !isGroupJidImpl(safeGroupJid)) {
31
+ return {
32
+ allowed: true,
33
+ ownerSessionId: null,
34
+ assignmentVersion: null,
35
+ reason: 'not_group',
36
+ };
37
+ }
38
+
39
+ const safeExpectedAssignmentVersion = normalizeAssignmentVersion(expectedAssignmentVersion);
40
+ const cacheKey = buildCacheKeyImpl(safeGroupJid, safeSessionId);
41
+ const mustBypassCache = bypassCache || Boolean(enforceFence && safeExpectedAssignmentVersion);
42
+
43
+ if (!mustBypassCache && cacheKey) {
44
+ const cached = cacheImpl.get(cacheKey);
45
+ if (cached && typeof cached === 'object') {
46
+ return cached;
47
+ }
48
+ }
49
+
50
+ try {
51
+ const ownerState = await getOwnerImpl(safeGroupJid, { bypassCache: mustBypassCache });
52
+ let ownerSessionId = String(ownerState?.ownerSessionId || '').trim() || null;
53
+ let assignmentVersion = normalizeAssignmentVersion(ownerState?.assignmentVersion);
54
+ let allowed = false;
55
+ let reason = 'owned_by_other';
56
+
57
+ if (!ownerSessionId && allowClaim) {
58
+ const claimOutcome = await tryAcquireImpl({
59
+ groupJid: safeGroupJid,
60
+ sessionId: safeSessionId,
61
+ reason: 'writer_gate_claim',
62
+ changedBy: safeSessionId,
63
+ metadata: {
64
+ source,
65
+ gate: 'group_write',
66
+ },
67
+ });
68
+
69
+ ownerSessionId = String(claimOutcome?.owner?.ownerSessionId || '').trim() || null;
70
+ assignmentVersion = normalizeAssignmentVersion(claimOutcome?.assignmentVersion ?? claimOutcome?.owner?.assignmentVersion);
71
+ allowed = Boolean(claimOutcome?.acquired && ownerSessionId === safeSessionId);
72
+ reason = claimOutcome?.reason || 'claim_attempt';
73
+ } else {
74
+ allowed = Boolean(ownerSessionId && ownerSessionId === safeSessionId);
75
+ reason = !ownerSessionId ? 'owner_missing' : allowed ? 'owner_match' : 'owned_by_other';
76
+ }
77
+
78
+ if (allowed && enforceFence && safeExpectedAssignmentVersion && assignmentVersion && assignmentVersion !== safeExpectedAssignmentVersion) {
79
+ allowed = false;
80
+ reason = 'fence_token_mismatch';
81
+ }
82
+
83
+ const resolved = {
84
+ allowed,
85
+ ownerSessionId,
86
+ assignmentVersion,
87
+ reason,
88
+ };
89
+
90
+ if (cacheKey) {
91
+ cacheImpl.set(cacheKey, resolved);
92
+ }
93
+ return resolved;
94
+ } catch (error) {
95
+ loggerImpl.warn('Falha ao resolver ownership para escrita de grupo.', {
96
+ action: 'group_owner_write_state_failed',
97
+ source,
98
+ sessionId: safeSessionId,
99
+ groupId: safeGroupJid,
100
+ error: error?.message,
101
+ });
102
+ return {
103
+ allowed: false,
104
+ ownerSessionId: null,
105
+ assignmentVersion: null,
106
+ reason: 'owner_resolution_failed',
107
+ };
108
+ }
109
+ };