@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.
- package/.env.example +24 -0
- package/app/config/index.js +4 -0
- package/app/configParts/adminIdentity.js +29 -0
- package/app/configParts/baileysConfig.js +116 -0
- package/app/configParts/groupUtils.js +221 -0
- package/app/configParts/loggerConfig.js +185 -0
- package/app/configParts/messagePersistenceService.js +169 -7
- package/app/configParts/sessionConfig.js +85 -0
- package/app/connection/baileysCompatibility.test.js +9 -0
- package/app/connection/baileysDbAuthState.js +205 -9
- package/app/connection/baileysLibsignalPatch.js +210 -0
- package/app/connection/groupOwnerWriteStateResolver.js +53 -21
- package/app/connection/socketController.js +95 -25
- package/app/connection/socketController.multiSession.test.js +20 -0
- package/app/controllers/messagePipeline/preProcessingMiddlewares.js +17 -3
- package/app/controllers/messageProcessingPipeline.js +2 -0
- package/app/controllers/messageProcessingPipeline.test.js +15 -13
- package/app/services/multiSession/assignmentBalancerService.js +1 -6
- package/app/services/multiSession/groupOwnershipRepository.js +9 -44
- package/app/services/multiSession/groupOwnershipService.js +9 -90
- package/app/services/multiSession/groupOwnershipService.test.js +12 -4
- package/app/services/multiSession/sessionRegistryService.js +6 -60
- package/app/utils/antiLink/antiLinkModule.js +54 -24
- package/docs/security/omnizap-static-security-headers.conf +3 -3
- package/package.json +3 -2
- package/public/comandos/commands-catalog.json +1 -1
- package/public/css/payments-react.css +478 -0
- package/public/js/apps/homeReactApp.js +2 -2
- package/public/js/apps/paymentsCancelReactApp.js +45 -0
- package/public/js/apps/paymentsReactApp.js +399 -0
- package/public/js/apps/paymentsSuccessReactApp.js +148 -0
- package/public/pages/pagamentos-cancelado.html +21 -0
- package/public/pages/pagamentos-sucesso.html +21 -0
- package/public/pages/pagamentos.html +30 -0
- package/scripts/deploy.sh +3 -0
- package/scripts/new-whatsapp-session.sh +247 -0
- package/server/controllers/admin/systemAdminController.js +4 -17
- package/server/controllers/payments/paymentsController.js +731 -0
- package/server/controllers/system/systemController.js +4 -30
- package/server/email/emailAutomationRuntime.js +36 -1
- package/server/email/emailAutomationService.js +42 -1
- package/server/email/emailTemplateService.js +137 -31
- package/server/http/httpRequestUtils.js +18 -14
- package/server/middleware/securityHeaders.js +15 -2
- package/server/routes/indexRouter.js +27 -7
- package/server/routes/payments/paymentsRouter.js +47 -0
- package/server/routes/static/staticPageRouter.js +3 -0
- 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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
163
|
-
|
|
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:
|
|
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
|
+
};
|