@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
@@ -0,0 +1,381 @@
1
+ import { executeQuery, TABLES } from '../../../database/index.js';
2
+
3
+ const GROUP_ASSIGNMENT_TABLE = TABLES.GROUP_ASSIGNMENT;
4
+ const GROUP_ASSIGNMENT_HISTORY_TABLE = TABLES.GROUP_ASSIGNMENT_HISTORY;
5
+ const MAX_GROUP_JID_LENGTH = 255;
6
+ const MAX_SESSION_ID_LENGTH = 64;
7
+ const MAX_REASON_LENGTH = 64;
8
+ const MAX_CHANGED_BY_LENGTH = 64;
9
+
10
+ const toDateOrNull = (value) => {
11
+ if (!value) return null;
12
+ if (value instanceof Date) {
13
+ return Number.isNaN(value.getTime()) ? null : value;
14
+ }
15
+ const parsed = new Date(value);
16
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
17
+ };
18
+
19
+ const toPositiveInt = (value, fallback = 1) => {
20
+ const parsed = Number.parseInt(String(value ?? ''), 10);
21
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
22
+ return parsed;
23
+ };
24
+
25
+ const toBool = (value) => value === true || value === 1 || value === '1';
26
+
27
+ const parseJson = (value) => {
28
+ if (!value) return null;
29
+ if (typeof value === 'object') return value;
30
+ try {
31
+ return JSON.parse(String(value));
32
+ } catch {
33
+ return null;
34
+ }
35
+ };
36
+
37
+ const serializeJson = (value) => {
38
+ if (value === undefined || value === null) return null;
39
+ if (typeof value === 'string') {
40
+ const normalized = value.trim();
41
+ return normalized || null;
42
+ }
43
+ try {
44
+ return JSON.stringify(value);
45
+ } catch {
46
+ return null;
47
+ }
48
+ };
49
+
50
+ export const normalizeGroupJid = (value) => {
51
+ const normalized = String(value || '')
52
+ .trim()
53
+ .slice(0, MAX_GROUP_JID_LENGTH);
54
+ return normalized || null;
55
+ };
56
+
57
+ export const normalizeSessionId = (value) => {
58
+ const normalized = String(value || '')
59
+ .trim()
60
+ .slice(0, MAX_SESSION_ID_LENGTH);
61
+ return normalized || null;
62
+ };
63
+
64
+ export const normalizeReason = (value) => {
65
+ if (value === undefined || value === null) return null;
66
+ const normalized = String(value)
67
+ .trim()
68
+ .slice(0, MAX_REASON_LENGTH);
69
+ return normalized || null;
70
+ };
71
+
72
+ export const normalizeChangedBy = (value) => {
73
+ const normalized = String(value || 'system')
74
+ .trim()
75
+ .slice(0, MAX_CHANGED_BY_LENGTH);
76
+ return normalized || 'system';
77
+ };
78
+
79
+ export const normalizeAssignmentRow = (row = null) => {
80
+ if (!row) return null;
81
+ return {
82
+ groupJid: normalizeGroupJid(row.group_jid),
83
+ ownerSessionId: normalizeSessionId(row.owner_session_id),
84
+ leaseExpiresAt: toDateOrNull(row.lease_expires_at),
85
+ cooldownUntil: toDateOrNull(row.cooldown_until),
86
+ assignmentVersion: toPositiveInt(row.assignment_version, 1),
87
+ pinned: toBool(row.pinned),
88
+ lastReason: normalizeReason(row.last_reason),
89
+ createdAt: toDateOrNull(row.created_at),
90
+ updatedAt: toDateOrNull(row.updated_at),
91
+ };
92
+ };
93
+
94
+ const ASSIGNMENT_SELECT_COLUMNS = `group_jid,
95
+ owner_session_id,
96
+ lease_expires_at,
97
+ cooldown_until,
98
+ assignment_version,
99
+ pinned,
100
+ last_reason,
101
+ created_at,
102
+ updated_at`;
103
+
104
+ const clampLimit = (value, fallback = 200, min = 1, max = 5_000) => {
105
+ const parsed = Number.parseInt(String(value ?? ''), 10);
106
+ if (!Number.isFinite(parsed)) return fallback;
107
+ return Math.max(min, Math.min(max, parsed));
108
+ };
109
+
110
+ export const getAssignment = async (groupJid, connection = null) => {
111
+ const safeGroupJid = normalizeGroupJid(groupJid);
112
+ if (!safeGroupJid) return null;
113
+
114
+ const rows = await executeQuery(
115
+ `SELECT ${ASSIGNMENT_SELECT_COLUMNS}
116
+ FROM ${GROUP_ASSIGNMENT_TABLE}
117
+ WHERE group_jid = ?
118
+ LIMIT 1`,
119
+ [safeGroupJid],
120
+ connection,
121
+ );
122
+
123
+ return normalizeAssignmentRow(rows?.[0] || null);
124
+ };
125
+
126
+ export const getAssignmentForUpdate = async (groupJid, connection) => {
127
+ const safeGroupJid = normalizeGroupJid(groupJid);
128
+ if (!safeGroupJid) return null;
129
+ if (!connection) {
130
+ throw new Error('getAssignmentForUpdate requer connection transacional.');
131
+ }
132
+
133
+ const rows = await executeQuery(
134
+ `SELECT ${ASSIGNMENT_SELECT_COLUMNS}
135
+ FROM ${GROUP_ASSIGNMENT_TABLE}
136
+ WHERE group_jid = ?
137
+ LIMIT 1
138
+ FOR UPDATE`,
139
+ [safeGroupJid],
140
+ connection,
141
+ );
142
+
143
+ return normalizeAssignmentRow(rows?.[0] || null);
144
+ };
145
+
146
+ export const listAssignments = async (
147
+ {
148
+ groupJid = null,
149
+ ownerSessionId = null,
150
+ includeExpired = true,
151
+ limit = 200,
152
+ } = {},
153
+ connection = null,
154
+ ) => {
155
+ const safeGroupJid = normalizeGroupJid(groupJid);
156
+ const safeOwnerSessionId = normalizeSessionId(ownerSessionId);
157
+ const safeLimit = clampLimit(limit, 200, 1, 5_000);
158
+
159
+ const where = [];
160
+ const params = [];
161
+
162
+ if (safeGroupJid) {
163
+ where.push('group_jid = ?');
164
+ params.push(safeGroupJid);
165
+ }
166
+
167
+ if (safeOwnerSessionId) {
168
+ where.push('owner_session_id = ?');
169
+ params.push(safeOwnerSessionId);
170
+ }
171
+
172
+ if (!includeExpired) {
173
+ where.push('lease_expires_at > UTC_TIMESTAMP()');
174
+ }
175
+
176
+ const whereSql = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
177
+ const rows = await executeQuery(
178
+ `SELECT ${ASSIGNMENT_SELECT_COLUMNS}
179
+ FROM ${GROUP_ASSIGNMENT_TABLE}
180
+ ${whereSql}
181
+ ORDER BY updated_at DESC
182
+ LIMIT ${safeLimit}`,
183
+ params,
184
+ connection,
185
+ );
186
+
187
+ return (Array.isArray(rows) ? rows : [])
188
+ .map((row) => normalizeAssignmentRow(row))
189
+ .filter(Boolean);
190
+ };
191
+
192
+ export const createAssignment = async (
193
+ { groupJid, ownerSessionId, leaseExpiresAt, cooldownUntil = null, pinned = false, reason = null, assignmentVersion = 1 } = {},
194
+ connection = null,
195
+ ) => {
196
+ const safeGroupJid = normalizeGroupJid(groupJid);
197
+ const safeOwnerSessionId = normalizeSessionId(ownerSessionId);
198
+ const safeLeaseExpiresAt = toDateOrNull(leaseExpiresAt);
199
+ if (!safeGroupJid || !safeOwnerSessionId || !safeLeaseExpiresAt) {
200
+ throw new Error('createAssignment requer groupJid, ownerSessionId e leaseExpiresAt validos.');
201
+ }
202
+
203
+ await executeQuery(
204
+ `INSERT INTO ${GROUP_ASSIGNMENT_TABLE}
205
+ (group_jid, owner_session_id, lease_expires_at, cooldown_until, assignment_version, pinned, last_reason)
206
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
207
+ [safeGroupJid, safeOwnerSessionId, safeLeaseExpiresAt, toDateOrNull(cooldownUntil), toPositiveInt(assignmentVersion, 1), toBool(pinned) ? 1 : 0, normalizeReason(reason)],
208
+ connection,
209
+ );
210
+
211
+ return getAssignment(safeGroupJid, connection);
212
+ };
213
+
214
+ export const updateAssignmentOwner = async (
215
+ { groupJid, ownerSessionId, leaseExpiresAt, reason = null, bumpVersion = true, cooldownUntil = undefined, pinned = undefined } = {},
216
+ connection = null,
217
+ ) => {
218
+ const safeGroupJid = normalizeGroupJid(groupJid);
219
+ const safeOwnerSessionId = normalizeSessionId(ownerSessionId);
220
+ const safeLeaseExpiresAt = toDateOrNull(leaseExpiresAt);
221
+
222
+ if (!safeGroupJid || !safeOwnerSessionId || !safeLeaseExpiresAt) {
223
+ throw new Error('updateAssignmentOwner requer groupJid, ownerSessionId e leaseExpiresAt validos.');
224
+ }
225
+
226
+ const sets = ['owner_session_id = ?', 'lease_expires_at = ?', 'last_reason = ?'];
227
+ const params = [safeOwnerSessionId, safeLeaseExpiresAt, normalizeReason(reason)];
228
+
229
+ if (cooldownUntil !== undefined) {
230
+ sets.push('cooldown_until = ?');
231
+ params.push(toDateOrNull(cooldownUntil));
232
+ }
233
+
234
+ if (pinned !== undefined) {
235
+ sets.push('pinned = ?');
236
+ params.push(toBool(pinned) ? 1 : 0);
237
+ }
238
+
239
+ if (bumpVersion) {
240
+ sets.push('assignment_version = assignment_version + 1');
241
+ }
242
+
243
+ params.push(safeGroupJid);
244
+ await executeQuery(
245
+ `UPDATE ${GROUP_ASSIGNMENT_TABLE}
246
+ SET ${sets.join(', ')}
247
+ WHERE group_jid = ?`,
248
+ params,
249
+ connection,
250
+ );
251
+
252
+ return getAssignment(safeGroupJid, connection);
253
+ };
254
+
255
+ export const updateAssignmentLease = async (
256
+ { groupJid, ownerSessionId, leaseExpiresAt, reason = undefined } = {},
257
+ connection = null,
258
+ ) => {
259
+ const safeGroupJid = normalizeGroupJid(groupJid);
260
+ const safeOwnerSessionId = normalizeSessionId(ownerSessionId);
261
+ const safeLeaseExpiresAt = toDateOrNull(leaseExpiresAt);
262
+ if (!safeGroupJid || !safeOwnerSessionId || !safeLeaseExpiresAt) {
263
+ throw new Error('updateAssignmentLease requer groupJid, ownerSessionId e leaseExpiresAt validos.');
264
+ }
265
+
266
+ const sets = ['lease_expires_at = ?'];
267
+ const params = [safeLeaseExpiresAt];
268
+
269
+ if (reason !== undefined) {
270
+ sets.push('last_reason = ?');
271
+ params.push(normalizeReason(reason));
272
+ }
273
+
274
+ params.push(safeGroupJid, safeOwnerSessionId);
275
+ await executeQuery(
276
+ `UPDATE ${GROUP_ASSIGNMENT_TABLE}
277
+ SET ${sets.join(', ')}
278
+ WHERE group_jid = ? AND owner_session_id = ?`,
279
+ params,
280
+ connection,
281
+ );
282
+
283
+ return getAssignment(safeGroupJid, connection);
284
+ };
285
+
286
+ export const expireAssignment = async (
287
+ { groupJid, ownerSessionId = null, reason = null, bumpVersion = true, leaseExpiresAt = new Date() } = {},
288
+ connection = null,
289
+ ) => {
290
+ const safeGroupJid = normalizeGroupJid(groupJid);
291
+ if (!safeGroupJid) {
292
+ throw new Error('expireAssignment requer groupJid valido.');
293
+ }
294
+
295
+ const safeOwnerSessionId = normalizeSessionId(ownerSessionId);
296
+ const safeLeaseExpiresAt = toDateOrNull(leaseExpiresAt) || new Date();
297
+ const sets = ['lease_expires_at = ?', 'last_reason = ?'];
298
+ if (bumpVersion) {
299
+ sets.push('assignment_version = assignment_version + 1');
300
+ }
301
+
302
+ const params = [safeLeaseExpiresAt, normalizeReason(reason), safeGroupJid];
303
+ let where = 'group_jid = ?';
304
+ if (safeOwnerSessionId) {
305
+ where += ' AND owner_session_id = ?';
306
+ params.push(safeOwnerSessionId);
307
+ }
308
+
309
+ await executeQuery(
310
+ `UPDATE ${GROUP_ASSIGNMENT_TABLE}
311
+ SET ${sets.join(', ')}
312
+ WHERE ${where}`,
313
+ params,
314
+ connection,
315
+ );
316
+
317
+ return getAssignment(safeGroupJid, connection);
318
+ };
319
+
320
+ export const renewLeasesByOwner = async (
321
+ {
322
+ ownerSessionId,
323
+ leaseExpiresAt,
324
+ reason = null,
325
+ now = undefined,
326
+ } = {},
327
+ connection = null,
328
+ ) => {
329
+ const safeOwnerSessionId = normalizeSessionId(ownerSessionId);
330
+ const safeLeaseExpiresAt = toDateOrNull(leaseExpiresAt);
331
+ const safeNow = now === undefined ? new Date() : toDateOrNull(now) || new Date();
332
+ if (!safeOwnerSessionId || !safeLeaseExpiresAt) {
333
+ throw new Error('renewLeasesByOwner requer ownerSessionId e leaseExpiresAt validos.');
334
+ }
335
+
336
+ const result = await executeQuery(
337
+ `UPDATE ${GROUP_ASSIGNMENT_TABLE}
338
+ SET lease_expires_at = ?,
339
+ last_reason = ?
340
+ WHERE owner_session_id = ?
341
+ AND lease_expires_at > ?`,
342
+ [safeLeaseExpiresAt, normalizeReason(reason), safeOwnerSessionId, safeNow],
343
+ connection,
344
+ );
345
+
346
+ return Number(result?.affectedRows || 0);
347
+ };
348
+
349
+ export const insertAssignmentHistory = async (
350
+ { groupJid, previousSessionId = null, newSessionId, changeReason = null, changedBy = 'system', assignmentVersion = 1, metadata = null } = {},
351
+ connection = null,
352
+ ) => {
353
+ const safeGroupJid = normalizeGroupJid(groupJid);
354
+ const safePreviousSessionId = normalizeSessionId(previousSessionId);
355
+ const safeNewSessionId = normalizeSessionId(newSessionId) || safePreviousSessionId;
356
+ const safeChangedBy = normalizeChangedBy(changedBy);
357
+ const safeVersion = toPositiveInt(assignmentVersion, 1);
358
+
359
+ if (!safeGroupJid || !safeNewSessionId) {
360
+ throw new Error('insertAssignmentHistory requer groupJid e newSessionId validos.');
361
+ }
362
+
363
+ const result = await executeQuery(
364
+ `INSERT INTO ${GROUP_ASSIGNMENT_HISTORY_TABLE}
365
+ (group_jid, previous_session_id, new_session_id, change_reason, changed_by, assignment_version, metadata)
366
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
367
+ [safeGroupJid, safePreviousSessionId, safeNewSessionId, normalizeReason(changeReason), safeChangedBy, safeVersion, serializeJson(metadata)],
368
+ connection,
369
+ );
370
+
371
+ return {
372
+ id: Number(result?.insertId || 0),
373
+ groupJid: safeGroupJid,
374
+ previousSessionId: safePreviousSessionId,
375
+ newSessionId: safeNewSessionId,
376
+ changeReason: normalizeReason(changeReason),
377
+ changedBy: safeChangedBy,
378
+ assignmentVersion: safeVersion,
379
+ metadata: parseJson(serializeJson(metadata)),
380
+ };
381
+ };