@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,309 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { createGroupOwnershipService } from './groupOwnershipService.js';
5
+ import { closePool } from '../../../database/index.js';
6
+
7
+ const cloneDate = (value) => (value instanceof Date ? new Date(value.getTime()) : null);
8
+
9
+ const createInMemoryRepository = () => {
10
+ const assignments = new Map();
11
+ const history = [];
12
+
13
+ const normalizeGroupJid = (value) => {
14
+ const normalized = String(value || '').trim().slice(0, 255);
15
+ return normalized || null;
16
+ };
17
+ const normalizeSessionId = (value) => {
18
+ const normalized = String(value || '').trim().slice(0, 64);
19
+ return normalized || null;
20
+ };
21
+ const normalizeReason = (value) => {
22
+ const normalized = String(value || '').trim().slice(0, 64);
23
+ return normalized || null;
24
+ };
25
+ const normalizeChangedBy = (value) => {
26
+ const normalized = String(value || 'system').trim().slice(0, 64);
27
+ return normalized || 'system';
28
+ };
29
+
30
+ const cloneAssignment = (row) => {
31
+ if (!row) return null;
32
+ return {
33
+ groupJid: row.groupJid,
34
+ ownerSessionId: row.ownerSessionId,
35
+ leaseExpiresAt: cloneDate(row.leaseExpiresAt),
36
+ cooldownUntil: cloneDate(row.cooldownUntil),
37
+ assignmentVersion: Number(row.assignmentVersion || 1),
38
+ pinned: row.pinned === true,
39
+ lastReason: row.lastReason || null,
40
+ createdAt: cloneDate(row.createdAt),
41
+ updatedAt: cloneDate(row.updatedAt),
42
+ };
43
+ };
44
+
45
+ const upsertAssignment = (assignment) => {
46
+ const now = new Date();
47
+ const current = assignments.get(assignment.groupJid);
48
+ const next = {
49
+ groupJid: assignment.groupJid,
50
+ ownerSessionId: assignment.ownerSessionId,
51
+ leaseExpiresAt: cloneDate(assignment.leaseExpiresAt),
52
+ cooldownUntil: cloneDate(assignment.cooldownUntil),
53
+ assignmentVersion: Number(assignment.assignmentVersion || 1),
54
+ pinned: assignment.pinned === true,
55
+ lastReason: assignment.lastReason || null,
56
+ createdAt: current?.createdAt ? cloneDate(current.createdAt) : now,
57
+ updatedAt: now,
58
+ };
59
+ assignments.set(next.groupJid, next);
60
+ return cloneAssignment(next);
61
+ };
62
+
63
+ return {
64
+ normalizeGroupJid,
65
+ normalizeSessionId,
66
+ normalizeReason,
67
+ normalizeChangedBy,
68
+ getAssignment: async (groupJid) => cloneAssignment(assignments.get(normalizeGroupJid(groupJid))),
69
+ getAssignmentForUpdate: async (groupJid) => cloneAssignment(assignments.get(normalizeGroupJid(groupJid))),
70
+ listAssignments: async ({ groupJid = null, ownerSessionId = null, includeExpired = true, limit = 200 } = {}) => {
71
+ const safeGroupJid = normalizeGroupJid(groupJid);
72
+ const safeOwnerSessionId = normalizeSessionId(ownerSessionId);
73
+ const nowMs = Date.now();
74
+ const rows = Array.from(assignments.values())
75
+ .filter((row) => (safeGroupJid ? row.groupJid === safeGroupJid : true))
76
+ .filter((row) => (safeOwnerSessionId ? row.ownerSessionId === safeOwnerSessionId : true))
77
+ .filter((row) => (includeExpired ? true : row.leaseExpiresAt?.getTime?.() > nowMs))
78
+ .slice(0, Math.max(1, Number(limit || 200)));
79
+ return rows.map((row) => cloneAssignment(row));
80
+ },
81
+ createAssignment: async ({ groupJid, ownerSessionId, leaseExpiresAt, cooldownUntil = null, pinned = false, reason = null, assignmentVersion = 1 } = {}) => {
82
+ const safeGroupJid = normalizeGroupJid(groupJid);
83
+ if (!safeGroupJid) {
84
+ throw new Error('groupJid invalido');
85
+ }
86
+ if (assignments.has(safeGroupJid)) {
87
+ const error = new Error('duplicate');
88
+ error.code = 'ER_DUP_ENTRY';
89
+ throw error;
90
+ }
91
+ return upsertAssignment({
92
+ groupJid: safeGroupJid,
93
+ ownerSessionId: normalizeSessionId(ownerSessionId),
94
+ leaseExpiresAt: cloneDate(leaseExpiresAt),
95
+ cooldownUntil: cloneDate(cooldownUntil),
96
+ assignmentVersion: Number(assignmentVersion || 1),
97
+ pinned: pinned === true,
98
+ lastReason: normalizeReason(reason),
99
+ });
100
+ },
101
+ updateAssignmentOwner: async ({ groupJid, ownerSessionId, leaseExpiresAt, reason = null, bumpVersion = true, cooldownUntil = undefined, pinned = undefined } = {}) => {
102
+ const safeGroupJid = normalizeGroupJid(groupJid);
103
+ const current = assignments.get(safeGroupJid);
104
+ if (!current) return null;
105
+ return upsertAssignment({
106
+ ...current,
107
+ ownerSessionId: normalizeSessionId(ownerSessionId) || current.ownerSessionId,
108
+ leaseExpiresAt: cloneDate(leaseExpiresAt),
109
+ cooldownUntil: cooldownUntil === undefined ? current.cooldownUntil : cloneDate(cooldownUntil),
110
+ pinned: pinned === undefined ? current.pinned : pinned === true,
111
+ lastReason: normalizeReason(reason),
112
+ assignmentVersion: bumpVersion ? Number(current.assignmentVersion || 1) + 1 : Number(current.assignmentVersion || 1),
113
+ });
114
+ },
115
+ updateAssignmentLease: async ({ groupJid, ownerSessionId, leaseExpiresAt, reason = undefined } = {}) => {
116
+ const safeGroupJid = normalizeGroupJid(groupJid);
117
+ const current = assignments.get(safeGroupJid);
118
+ if (!current) return null;
119
+ if (current.ownerSessionId !== normalizeSessionId(ownerSessionId)) return cloneAssignment(current);
120
+ return upsertAssignment({
121
+ ...current,
122
+ leaseExpiresAt: cloneDate(leaseExpiresAt),
123
+ lastReason: reason === undefined ? current.lastReason : normalizeReason(reason),
124
+ });
125
+ },
126
+ expireAssignment: async ({ groupJid, ownerSessionId = null, reason = null, bumpVersion = true, leaseExpiresAt = new Date() } = {}) => {
127
+ const safeGroupJid = normalizeGroupJid(groupJid);
128
+ const current = assignments.get(safeGroupJid);
129
+ if (!current) return null;
130
+ const safeOwnerSessionId = normalizeSessionId(ownerSessionId);
131
+ if (safeOwnerSessionId && current.ownerSessionId !== safeOwnerSessionId) return cloneAssignment(current);
132
+ return upsertAssignment({
133
+ ...current,
134
+ leaseExpiresAt: cloneDate(leaseExpiresAt),
135
+ lastReason: normalizeReason(reason),
136
+ assignmentVersion: bumpVersion ? Number(current.assignmentVersion || 1) + 1 : Number(current.assignmentVersion || 1),
137
+ });
138
+ },
139
+ renewLeasesByOwner: async ({ ownerSessionId, leaseExpiresAt, reason = null, now = undefined } = {}) => {
140
+ const safeOwnerSessionId = normalizeSessionId(ownerSessionId);
141
+ const safeNow = now instanceof Date ? now.getTime() : Date.now();
142
+ let renewed = 0;
143
+ for (const current of assignments.values()) {
144
+ if (current.ownerSessionId !== safeOwnerSessionId) continue;
145
+ if ((current.leaseExpiresAt?.getTime?.() || 0) <= safeNow) continue;
146
+ upsertAssignment({
147
+ ...current,
148
+ leaseExpiresAt: cloneDate(leaseExpiresAt),
149
+ lastReason: normalizeReason(reason),
150
+ });
151
+ renewed += 1;
152
+ }
153
+ return renewed;
154
+ },
155
+ insertAssignmentHistory: async ({ groupJid, previousSessionId = null, newSessionId, changeReason = null, changedBy = 'system', assignmentVersion = 1, metadata = null } = {}) => {
156
+ history.push({
157
+ groupJid: normalizeGroupJid(groupJid),
158
+ previousSessionId: normalizeSessionId(previousSessionId),
159
+ newSessionId: normalizeSessionId(newSessionId),
160
+ changeReason: normalizeReason(changeReason),
161
+ changedBy: normalizeChangedBy(changedBy),
162
+ assignmentVersion: Number(assignmentVersion || 1),
163
+ metadata,
164
+ });
165
+ return { id: history.length };
166
+ },
167
+ __state: {
168
+ assignments,
169
+ history,
170
+ },
171
+ };
172
+ };
173
+
174
+ const createSessionRegistryMock = () => ({
175
+ ensureSession: async () => ({ ok: true }),
176
+ heartbeatSession: async () => ({ ok: true }),
177
+ });
178
+
179
+ const createService = ({ nowRef }) => {
180
+ const repository = createInMemoryRepository();
181
+ const sessionRegistry = createSessionRegistryMock();
182
+ const service = createGroupOwnershipService({
183
+ repository,
184
+ sessionRegistry,
185
+ withTransactionImpl: async (handler) => handler({}),
186
+ nowImpl: () => nowRef.value,
187
+ loggerImpl: { warn: () => {} },
188
+ cacheTtlMs: 1,
189
+ });
190
+ return { service, repository };
191
+ };
192
+
193
+ test.after(async () => {
194
+ await new Promise((resolve) => {
195
+ setTimeout(resolve, 200);
196
+ });
197
+ await closePool();
198
+ });
199
+
200
+ test('groupOwnershipService: claim concorrente no mesmo grupo resulta em owner unico', async () => {
201
+ const nowRef = { value: 1_000 };
202
+ const { service } = createService({ nowRef });
203
+
204
+ const [left, right] = await Promise.all([
205
+ service.tryAcquire({
206
+ groupJid: '120363222222222222@g.us',
207
+ sessionId: 'session-a',
208
+ reason: 'claim_a',
209
+ }),
210
+ service.tryAcquire({
211
+ groupJid: '120363222222222222@g.us',
212
+ sessionId: 'session-b',
213
+ reason: 'claim_b',
214
+ }),
215
+ ]);
216
+
217
+ const acquiredCount = [left, right].filter((item) => item?.acquired).length;
218
+ assert.equal(acquiredCount, 1);
219
+
220
+ const owner = await service.getOwner('120363222222222222@g.us', { bypassCache: true });
221
+ assert.ok(owner);
222
+ assert.equal(owner.assignmentVersion, 1);
223
+ assert.ok(owner.ownerSessionId === 'session-a' || owner.ownerSessionId === 'session-b');
224
+ });
225
+
226
+ test('groupOwnershipService: heartbeat renova lease e failover ocorre apos expirar', async () => {
227
+ const nowRef = { value: 10_000 };
228
+ const { service } = createService({ nowRef });
229
+ const groupJid = '120363333333333333@g.us';
230
+
231
+ const firstClaim = await service.tryAcquire({
232
+ groupJid,
233
+ sessionId: 'session-a',
234
+ leaseMs: 2_000,
235
+ });
236
+ assert.equal(firstClaim.acquired, true);
237
+ assert.equal(firstClaim.assignmentVersion, 1);
238
+
239
+ nowRef.value += 1_000;
240
+ const heartbeat = await service.heartbeatOwnerSession({
241
+ sessionId: 'session-a',
242
+ leaseMs: 2_000,
243
+ reason: 'test_heartbeat',
244
+ });
245
+ assert.ok(heartbeat.renewedAssignments >= 1);
246
+
247
+ nowRef.value = heartbeat.leaseExpiresAt.getTime() + 10;
248
+ const failover = await service.tryAcquire({
249
+ groupJid,
250
+ sessionId: 'session-b',
251
+ leaseMs: 2_000,
252
+ reason: 'failover_after_expiry',
253
+ });
254
+
255
+ assert.equal(failover.acquired, true);
256
+ assert.equal(failover.reason, 'reassigned');
257
+ assert.equal(failover.assignmentVersion, 2);
258
+
259
+ const owner = await service.getOwner(groupJid, { bypassCache: true });
260
+ assert.equal(owner?.ownerSessionId, 'session-b');
261
+ assert.equal(owner?.assignmentVersion, 2);
262
+ });
263
+
264
+ test('groupOwnershipService: fence token com assignment_version invalida sessao com token antigo', async () => {
265
+ const nowRef = { value: 50_000 };
266
+ const { service } = createService({ nowRef });
267
+ const groupJid = '120363444444444444@g.us';
268
+
269
+ const claimed = await service.tryAcquire({
270
+ groupJid,
271
+ sessionId: 'session-a',
272
+ leaseMs: 5_000,
273
+ reason: 'initial_claim',
274
+ });
275
+ assert.equal(claimed.acquired, true);
276
+ assert.equal(claimed.assignmentVersion, 1);
277
+
278
+ const tokenBefore = service.buildFencingToken({
279
+ groupJid,
280
+ ownerSessionId: 'session-a',
281
+ assignmentVersion: 1,
282
+ });
283
+ assert.equal(tokenBefore, `${groupJid}:session-a:1`);
284
+
285
+ const forced = await service.forceAssign({
286
+ groupJid,
287
+ sessionId: 'session-b',
288
+ reason: 'forced_failover',
289
+ changedBy: 'test',
290
+ });
291
+ assert.equal(forced.reassigned, true);
292
+ assert.equal(forced.assignmentVersion, 2);
293
+
294
+ const oldTokenValidation = await service.validateFenceToken({
295
+ groupJid,
296
+ sessionId: 'session-a',
297
+ assignmentVersion: 1,
298
+ bypassCache: true,
299
+ });
300
+ assert.equal(oldTokenValidation.valid, false);
301
+
302
+ const newTokenValidation = await service.validateFenceToken({
303
+ groupJid,
304
+ sessionId: 'session-b',
305
+ assignmentVersion: 2,
306
+ bypassCache: true,
307
+ });
308
+ assert.equal(newTokenValidation.valid, true);
309
+ });
@@ -0,0 +1,293 @@
1
+ import { executeQuery, TABLES } from '../../../database/index.js';
2
+ import { normalizeSessionId } from './groupOwnershipRepository.js';
3
+
4
+ const SESSION_REGISTRY_TABLE = TABLES.WA_SESSION_REGISTRY;
5
+ const MAX_STATUS_LENGTH = 24;
6
+ const MAX_BOT_JID_LENGTH = 255;
7
+ const DEFAULT_STATUS = 'offline';
8
+ const DEFAULT_WEIGHT = 1;
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 = DEFAULT_WEIGHT, min = 1, max = 10_000) => {
20
+ const parsed = Number.parseInt(String(value ?? ''), 10);
21
+ if (!Number.isFinite(parsed)) return fallback;
22
+ return Math.max(min, Math.min(max, parsed));
23
+ };
24
+
25
+ const toNumber = (value, fallback = 0) => {
26
+ const parsed = Number(value);
27
+ return Number.isFinite(parsed) ? parsed : fallback;
28
+ };
29
+
30
+ const normalizeStatus = (value, fallback = DEFAULT_STATUS) => {
31
+ const normalized = String(value || fallback)
32
+ .trim()
33
+ .toLowerCase()
34
+ .slice(0, MAX_STATUS_LENGTH);
35
+ return normalized || fallback;
36
+ };
37
+
38
+ const normalizeBotJid = (value) => {
39
+ if (value === undefined) return undefined;
40
+ if (value === null) return null;
41
+ const normalized = String(value)
42
+ .trim()
43
+ .toLowerCase()
44
+ .slice(0, MAX_BOT_JID_LENGTH);
45
+ return normalized || null;
46
+ };
47
+
48
+ const parseJson = (value) => {
49
+ if (!value) return null;
50
+ if (typeof value === 'object') return value;
51
+ try {
52
+ return JSON.parse(String(value));
53
+ } catch {
54
+ return null;
55
+ }
56
+ };
57
+
58
+ const serializeJson = (value) => {
59
+ if (value === undefined) return null;
60
+ if (value === null) return null;
61
+ if (typeof value === 'string') {
62
+ const normalized = value.trim();
63
+ return normalized || null;
64
+ }
65
+ try {
66
+ return JSON.stringify(value);
67
+ } catch {
68
+ return null;
69
+ }
70
+ };
71
+
72
+ const normalizeSessionRow = (row = null) => {
73
+ if (!row) return null;
74
+ return {
75
+ sessionId: normalizeSessionId(row.session_id),
76
+ botJid: normalizeBotJid(row.bot_jid) ?? null,
77
+ status: normalizeStatus(row.status, DEFAULT_STATUS),
78
+ capacityWeight: toPositiveInt(row.capacity_weight, DEFAULT_WEIGHT),
79
+ currentScore: toNumber(row.current_score, 0),
80
+ lastHeartbeatAt: toDateOrNull(row.last_heartbeat_at),
81
+ lastConnectedAt: toDateOrNull(row.last_connected_at),
82
+ lastDisconnectedAt: toDateOrNull(row.last_disconnected_at),
83
+ metadata: parseJson(row.metadata),
84
+ createdAt: toDateOrNull(row.created_at),
85
+ updatedAt: toDateOrNull(row.updated_at),
86
+ };
87
+ };
88
+
89
+ const SESSION_SELECT_COLUMNS = `session_id,
90
+ bot_jid,
91
+ status,
92
+ capacity_weight,
93
+ current_score,
94
+ last_heartbeat_at,
95
+ last_connected_at,
96
+ last_disconnected_at,
97
+ metadata,
98
+ created_at,
99
+ updated_at`;
100
+
101
+ export const getSession = async (sessionId, { connection = null } = {}) => {
102
+ const safeSessionId = normalizeSessionId(sessionId);
103
+ if (!safeSessionId) return null;
104
+
105
+ const rows = await executeQuery(
106
+ `SELECT ${SESSION_SELECT_COLUMNS}
107
+ FROM ${SESSION_REGISTRY_TABLE}
108
+ WHERE session_id = ?
109
+ LIMIT 1`,
110
+ [safeSessionId],
111
+ connection,
112
+ );
113
+
114
+ return normalizeSessionRow(rows?.[0] || null);
115
+ };
116
+
117
+ export const listSessions = async ({ status = null, limit = 100, connection = null } = {}) => {
118
+ const safeLimit = Math.max(1, Math.min(2_000, toPositiveInt(limit, 100, 1, 2_000)));
119
+ const safeStatus = status ? normalizeStatus(status, '') : '';
120
+
121
+ const params = [];
122
+ let where = '';
123
+ if (safeStatus) {
124
+ where = 'WHERE status = ?';
125
+ params.push(safeStatus);
126
+ }
127
+
128
+ const rows = await executeQuery(
129
+ `SELECT ${SESSION_SELECT_COLUMNS}
130
+ FROM ${SESSION_REGISTRY_TABLE}
131
+ ${where}
132
+ ORDER BY updated_at DESC
133
+ LIMIT ${safeLimit}`,
134
+ params,
135
+ connection,
136
+ );
137
+
138
+ return (Array.isArray(rows) ? rows : []).map((row) => normalizeSessionRow(row));
139
+ };
140
+
141
+ export const upsertSession = async (
142
+ {
143
+ sessionId,
144
+ botJid = undefined,
145
+ status = DEFAULT_STATUS,
146
+ capacityWeight = DEFAULT_WEIGHT,
147
+ currentScore = 0,
148
+ metadata = undefined,
149
+ heartbeatAt = undefined,
150
+ connectedAt = undefined,
151
+ disconnectedAt = undefined,
152
+ } = {},
153
+ { connection = null } = {},
154
+ ) => {
155
+ const safeSessionId = normalizeSessionId(sessionId);
156
+ if (!safeSessionId) {
157
+ throw new Error('upsertSession requer sessionId valido.');
158
+ }
159
+
160
+ const safeBotJid = normalizeBotJid(botJid);
161
+ const safeStatus = normalizeStatus(status, DEFAULT_STATUS);
162
+ const safeCapacityWeight = toPositiveInt(capacityWeight, DEFAULT_WEIGHT);
163
+ const safeCurrentScore = toNumber(currentScore, 0);
164
+ const safeMetadata = serializeJson(metadata);
165
+ const safeHeartbeatAt = heartbeatAt === undefined ? null : toDateOrNull(heartbeatAt);
166
+ const safeConnectedAt = connectedAt === undefined ? null : toDateOrNull(connectedAt);
167
+ const safeDisconnectedAt = disconnectedAt === undefined ? null : toDateOrNull(disconnectedAt);
168
+
169
+ await executeQuery(
170
+ `INSERT INTO ${SESSION_REGISTRY_TABLE}
171
+ (session_id, bot_jid, status, capacity_weight, current_score, last_heartbeat_at, last_connected_at, last_disconnected_at, metadata)
172
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
173
+ ON DUPLICATE KEY UPDATE
174
+ bot_jid = COALESCE(VALUES(bot_jid), bot_jid),
175
+ status = VALUES(status),
176
+ capacity_weight = VALUES(capacity_weight),
177
+ current_score = VALUES(current_score),
178
+ last_heartbeat_at = COALESCE(VALUES(last_heartbeat_at), last_heartbeat_at),
179
+ last_connected_at = COALESCE(VALUES(last_connected_at), last_connected_at),
180
+ last_disconnected_at = COALESCE(VALUES(last_disconnected_at), last_disconnected_at),
181
+ metadata = COALESCE(VALUES(metadata), metadata),
182
+ updated_at = CURRENT_TIMESTAMP`,
183
+ [safeSessionId, safeBotJid, safeStatus, safeCapacityWeight, safeCurrentScore, safeHeartbeatAt, safeConnectedAt, safeDisconnectedAt, safeMetadata],
184
+ connection,
185
+ );
186
+
187
+ return getSession(safeSessionId, { connection });
188
+ };
189
+
190
+ export const ensureSession = async (
191
+ sessionId,
192
+ {
193
+ status = 'online',
194
+ capacityWeight = DEFAULT_WEIGHT,
195
+ currentScore = 0,
196
+ metadata = undefined,
197
+ botJid = undefined,
198
+ connection = null,
199
+ } = {},
200
+ ) =>
201
+ upsertSession(
202
+ {
203
+ sessionId,
204
+ status,
205
+ capacityWeight,
206
+ currentScore,
207
+ metadata,
208
+ botJid,
209
+ },
210
+ { connection },
211
+ );
212
+
213
+ export const heartbeatSession = async (
214
+ sessionId,
215
+ {
216
+ status = 'online',
217
+ currentScore = 0,
218
+ metadata = undefined,
219
+ botJid = undefined,
220
+ capacityWeight = DEFAULT_WEIGHT,
221
+ connection = null,
222
+ } = {},
223
+ ) =>
224
+ upsertSession(
225
+ {
226
+ sessionId,
227
+ status,
228
+ currentScore,
229
+ metadata,
230
+ botJid,
231
+ capacityWeight,
232
+ heartbeatAt: new Date(),
233
+ },
234
+ { connection },
235
+ );
236
+
237
+ export const markSessionConnected = async (
238
+ sessionId,
239
+ {
240
+ botJid = undefined,
241
+ currentScore = 0,
242
+ metadata = undefined,
243
+ capacityWeight = DEFAULT_WEIGHT,
244
+ connection = null,
245
+ } = {},
246
+ ) =>
247
+ upsertSession(
248
+ {
249
+ sessionId,
250
+ botJid,
251
+ status: 'online',
252
+ currentScore,
253
+ metadata,
254
+ capacityWeight,
255
+ heartbeatAt: new Date(),
256
+ connectedAt: new Date(),
257
+ },
258
+ { connection },
259
+ );
260
+
261
+ export const markSessionDisconnected = async (
262
+ sessionId,
263
+ {
264
+ status = 'offline',
265
+ currentScore = 0,
266
+ metadata = undefined,
267
+ capacityWeight = DEFAULT_WEIGHT,
268
+ connection = null,
269
+ } = {},
270
+ ) =>
271
+ upsertSession(
272
+ {
273
+ sessionId,
274
+ status,
275
+ currentScore,
276
+ metadata,
277
+ capacityWeight,
278
+ disconnectedAt: new Date(),
279
+ },
280
+ { connection },
281
+ );
282
+
283
+ const sessionRegistryService = {
284
+ getSession,
285
+ listSessions,
286
+ upsertSession,
287
+ ensureSession,
288
+ heartbeatSession,
289
+ markSessionConnected,
290
+ markSessionDisconnected,
291
+ };
292
+
293
+ export default sessionRegistryService;
@@ -1,36 +1,53 @@
1
- import groupConfigStore from './groupConfigStore.js';
1
+ import { TABLES, executeQuery, findById, remove, upsert } from '../../database/index.js';
2
+ import { normalizeJid } from '../config/index.js';
2
3
 
3
- const PROMPT_CONFIG_ID = 'system:ai_prompts';
4
+ const AI_PROMPTS_TABLE = TABLES.SYSTEM_AI_PROMPTS;
5
+ const SELECT_AI_PROMPTS_SQL = `SELECT id, prompt FROM \`${AI_PROMPTS_TABLE}\` ORDER BY id ASC`;
4
6
 
5
- const normalizeMap = (map) => (map && typeof map === 'object' ? map : {});
7
+ const normalizePromptJid = (jid) => {
8
+ const raw = String(jid || '').trim();
9
+ if (!raw) return '';
10
+ return normalizeJid(raw) || raw;
11
+ };
12
+
13
+ const normalizePromptValue = (prompt) => {
14
+ if (typeof prompt === 'string') return prompt;
15
+ if (prompt === null || prompt === undefined) return '';
16
+ return String(prompt);
17
+ };
6
18
 
7
19
  const aiPromptStore = {
8
20
  getAllPrompts: async function () {
9
- const config = await groupConfigStore.getGroupConfig(PROMPT_CONFIG_ID);
10
- return normalizeMap(config.prompts);
21
+ const rows = await executeQuery(SELECT_AI_PROMPTS_SQL);
22
+ const prompts = {};
23
+ for (const row of rows) {
24
+ const promptJid = normalizePromptJid(row?.id);
25
+ if (!promptJid) continue;
26
+ prompts[promptJid] = normalizePromptValue(row?.prompt);
27
+ }
28
+ return prompts;
11
29
  },
12
30
 
13
31
  getPrompt: async function (jid) {
14
- if (!jid) return null;
15
- const prompts = await this.getAllPrompts();
16
- return prompts[jid] || null;
32
+ const normalizedJid = normalizePromptJid(jid);
33
+ if (!normalizedJid) return null;
34
+ const row = await findById(AI_PROMPTS_TABLE, normalizedJid);
35
+ if (!row) return null;
36
+ return normalizePromptValue(row.prompt);
17
37
  },
18
38
 
19
39
  setPrompt: async function (jid, prompt) {
20
- if (!jid) return null;
21
- const prompts = await this.getAllPrompts();
22
- prompts[jid] = prompt;
23
- await groupConfigStore.updateGroupConfig(PROMPT_CONFIG_ID, { prompts });
24
- return prompt;
40
+ const normalizedJid = normalizePromptJid(jid);
41
+ if (!normalizedJid) return null;
42
+ const normalizedPrompt = normalizePromptValue(prompt);
43
+ await upsert(AI_PROMPTS_TABLE, { id: normalizedJid, prompt: normalizedPrompt });
44
+ return normalizedPrompt;
25
45
  },
26
46
 
27
47
  clearPrompt: async function (jid) {
28
- if (!jid) return null;
29
- const prompts = await this.getAllPrompts();
30
- if (prompts[jid]) {
31
- delete prompts[jid];
32
- await groupConfigStore.updateGroupConfig(PROMPT_CONFIG_ID, { prompts });
33
- }
48
+ const normalizedJid = normalizePromptJid(jid);
49
+ if (!normalizedJid) return null;
50
+ await remove(AI_PROMPTS_TABLE, normalizedJid);
34
51
  return true;
35
52
  },
36
53
  };