@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
@@ -1,7 +1,8 @@
1
1
  import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
2
2
  import { randomUUID, randomBytes, scryptSync, timingSafeEqual } from 'node:crypto';
3
- import { URLSearchParams } from 'node:url';
3
+ import { URL, URLSearchParams } from 'node:url';
4
4
 
5
+ import { setAdminOverviewSnapshot } from '../../../app/observability/metrics.js';
5
6
  import { parseAdminModeratorUpsertPayload, parseAdminSessionPasswordPayload } from '../../auth/validation/authSchemas.js';
6
7
 
7
8
  export const createStickerCatalogAdminHandlers = ({ executeQuery, tables, logger, sendJson, readJsonBody, parseCookies, getCookieValuesFromRequest, appendSetCookie, buildCookieString, sanitizeText, normalizeGoogleSubject, normalizeEmail, normalizeJid, toIsoOrNull, toWhatsAppPhoneDigits, mapGoogleSessionResponseData, resolveGoogleWebSessionFromRequest, revokeGoogleWebSessionsByIdentity, getMarketplaceGlobalStatsCached, getSystemSummaryCached, getFeatureFlagsSnapshot, refreshFeatureFlags, listAdminBans, createAdminBanRecord, revokeAdminBanRecord, normalizeVisitPath, stickerWebPath, findStickerPackByPackKey, stickerPackService, buildManagedPackResponseData, sendManagedMutationStatus, sendManagedPackMutationStatus, deleteManagedPackWithCleanup, mapStickerPackWebManageError, cleanupOrphanStickerAssets, invalidateStickerCatalogDerivedCaches }) => {
@@ -16,6 +17,229 @@ export const createStickerCatalogAdminHandlers = ({ executeQuery, tables, logger
16
17
  const ADMIN_PANEL_SESSION_TTL_MS = Math.max(10 * 60 * 1000, Number(process.env.ADM_PANEL_SESSION_TTL_MS) || 12 * 60 * 60 * 1000);
17
18
  const ADMIN_MODERATOR_PASSWORD_MIN_LENGTH = Math.max(6, Number(process.env.ADM_MODERATOR_PASSWORD_MIN_LENGTH) || 8);
18
19
  const ADMIN_PANEL_SESSION_COOKIE_NAME = 'omnizap_admin_panel_session';
20
+ const SYSTEM_ADMIN_GRAFANA_TIME_FROM = sanitizeText(process.env.SYSTEM_ADMIN_GRAFANA_TIME_FROM || 'now-6h', 40, { allowEmpty: true }) || 'now-6h';
21
+ const SYSTEM_ADMIN_GRAFANA_TIME_TO = sanitizeText(process.env.SYSTEM_ADMIN_GRAFANA_TIME_TO || 'now', 40, { allowEmpty: true }) || 'now';
22
+ const SYSTEM_ADMIN_GRAFANA_REFRESH = sanitizeText(process.env.SYSTEM_ADMIN_GRAFANA_REFRESH || '10s', 20, { allowEmpty: true }) || '10s';
23
+ const SYSTEM_ADMIN_GRAFANA_STATUS_TIMEOUT_MS = Math.max(800, Number(process.env.SYSTEM_ADMIN_GRAFANA_STATUS_TIMEOUT_MS) || 2500);
24
+ const SYSTEM_ADMIN_GRAFANA_STATUS_CACHE_TTL_MS = Math.max(5000, Number(process.env.SYSTEM_ADMIN_GRAFANA_STATUS_CACHE_TTL_MS) || 15000);
25
+
26
+ const normalizeGrafanaBaseUrl = (value) => {
27
+ const raw = String(value || '').trim();
28
+ if (!raw) return '';
29
+ try {
30
+ const parsed = new URL(raw);
31
+ if (!['http:', 'https:'].includes(parsed.protocol)) return '';
32
+ const normalizedPathname = String(parsed.pathname || '/').replace(/\/+$/, '');
33
+ parsed.search = '';
34
+ parsed.hash = '';
35
+ return `${parsed.origin}${normalizedPathname === '/' ? '' : normalizedPathname}`;
36
+ } catch {
37
+ return '';
38
+ }
39
+ };
40
+
41
+ const normalizeGrafanaUid = (value) =>
42
+ String(value || '')
43
+ .trim()
44
+ .replace(/[^a-zA-Z0-9_-]/g, '')
45
+ .slice(0, 120);
46
+
47
+ const slugifyGrafanaDashboard = (value) => {
48
+ const raw = String(value || '')
49
+ .trim()
50
+ .toLowerCase();
51
+ const slug = raw
52
+ .replace(/[^a-z0-9]+/g, '-')
53
+ .replace(/^-+|-+$/g, '')
54
+ .slice(0, 90);
55
+ return slug || 'dashboard';
56
+ };
57
+
58
+ const parseGrafanaDashboardDefinitions = () => {
59
+ const raw = String(process.env.SYSTEM_ADMIN_GRAFANA_DASHBOARDS || '')
60
+ .split(',')
61
+ .map((item) => String(item || '').trim())
62
+ .filter(Boolean);
63
+
64
+ const parsed = raw
65
+ .map((entry) => {
66
+ const [uidPart, ...titleParts] = entry.split('|');
67
+ const uid = normalizeGrafanaUid(uidPart);
68
+ const title = sanitizeText(titleParts.join('|') || '', 90, { allowEmpty: true }) || '';
69
+ return uid ? { uid, title } : null;
70
+ })
71
+ .filter(Boolean);
72
+
73
+ if (parsed.length) return parsed;
74
+
75
+ return [
76
+ { uid: 'omnizap-system-admin', title: 'System Admin' },
77
+ { uid: 'omnizap-overview', title: 'Overview' },
78
+ { uid: 'omnizap-mysql', title: 'MySQL' },
79
+ ];
80
+ };
81
+
82
+ const buildAdminGrafanaObservabilityLinks = () => {
83
+ const baseUrl = normalizeGrafanaBaseUrl(process.env.SYSTEM_ADMIN_GRAFANA_URL || process.env.GRAFANA_PUBLIC_URL || '');
84
+ if (!baseUrl) {
85
+ return {
86
+ enabled: false,
87
+ base_url: null,
88
+ dashboards: [],
89
+ };
90
+ }
91
+
92
+ const base = new URL(`${baseUrl}/`);
93
+ const definitions = parseGrafanaDashboardDefinitions();
94
+
95
+ const dashboards = definitions
96
+ .map((entry, index) => {
97
+ const uid = normalizeGrafanaUid(entry?.uid || '');
98
+ if (!uid) return null;
99
+ const title = sanitizeText(entry?.title || '', 90, { allowEmpty: true }) || `Dashboard ${index + 1}`;
100
+ const slug = slugifyGrafanaDashboard(title || uid);
101
+ const viewUrl = new URL(`d/${encodeURIComponent(uid)}/${encodeURIComponent(slug)}`, base);
102
+ viewUrl.searchParams.set('orgId', '1');
103
+ viewUrl.searchParams.set('from', SYSTEM_ADMIN_GRAFANA_TIME_FROM);
104
+ viewUrl.searchParams.set('to', SYSTEM_ADMIN_GRAFANA_TIME_TO);
105
+ viewUrl.searchParams.set('refresh', SYSTEM_ADMIN_GRAFANA_REFRESH);
106
+
107
+ const embedUrl = new URL(String(viewUrl));
108
+ embedUrl.searchParams.set('kiosk', 'tv');
109
+
110
+ return {
111
+ uid,
112
+ title,
113
+ view_url: viewUrl.toString(),
114
+ embed_url: embedUrl.toString(),
115
+ };
116
+ })
117
+ .filter(Boolean);
118
+
119
+ return {
120
+ enabled: dashboards.length > 0,
121
+ base_url: baseUrl,
122
+ dashboards,
123
+ };
124
+ };
125
+ const grafanaObservabilityLinks = buildAdminGrafanaObservabilityLinks();
126
+ let grafanaRuntimeSnapshotCache = {
127
+ expires_at_ms: 0,
128
+ payload: null,
129
+ };
130
+
131
+ const resolveGrafanaHealthEndpointUrl = () => {
132
+ const candidates = [process.env.GRAFANA_PROXY_TARGET_URL, process.env.GRAFANA_INTERNAL_URL, process.env.SYSTEM_ADMIN_GRAFANA_URL, process.env.GRAFANA_PUBLIC_URL];
133
+ for (const candidate of candidates) {
134
+ const baseUrl = normalizeGrafanaBaseUrl(candidate);
135
+ if (!baseUrl) continue;
136
+ try {
137
+ return new URL('api/health', `${baseUrl}/`).toString();
138
+ } catch {
139
+ // noop
140
+ }
141
+ }
142
+ return '';
143
+ };
144
+
145
+ const getGrafanaRuntimeSnapshot = async () => {
146
+ const nowMs = __timeNowMs();
147
+ if (grafanaRuntimeSnapshotCache.payload && nowMs < grafanaRuntimeSnapshotCache.expires_at_ms) {
148
+ return grafanaRuntimeSnapshotCache.payload;
149
+ }
150
+
151
+ const dashboardsTotal = Array.isArray(grafanaObservabilityLinks?.dashboards) ? grafanaObservabilityLinks.dashboards.length : 0;
152
+ const baseSnapshot = {
153
+ enabled: Boolean(grafanaObservabilityLinks?.enabled),
154
+ available: false,
155
+ status: 'unavailable',
156
+ response_ms: null,
157
+ checked_at: __timeNowIso(),
158
+ http_status: null,
159
+ version: null,
160
+ database: null,
161
+ commit: null,
162
+ dashboards_total: dashboardsTotal,
163
+ error: null,
164
+ };
165
+
166
+ const healthEndpointUrl = resolveGrafanaHealthEndpointUrl();
167
+ if (!healthEndpointUrl) {
168
+ grafanaRuntimeSnapshotCache = {
169
+ expires_at_ms: nowMs + SYSTEM_ADMIN_GRAFANA_STATUS_CACHE_TTL_MS,
170
+ payload: baseSnapshot,
171
+ };
172
+ return baseSnapshot;
173
+ }
174
+
175
+ const controller = typeof AbortController === 'function' ? new AbortController() : null;
176
+ const timeoutId =
177
+ controller && Number.isFinite(SYSTEM_ADMIN_GRAFANA_STATUS_TIMEOUT_MS)
178
+ ? setTimeout(() => {
179
+ controller.abort();
180
+ }, SYSTEM_ADMIN_GRAFANA_STATUS_TIMEOUT_MS)
181
+ : null;
182
+ const startedAtMs = __timeNowMs();
183
+
184
+ try {
185
+ const response = await globalThis.fetch(healthEndpointUrl, {
186
+ method: 'GET',
187
+ headers: {
188
+ Accept: 'application/json',
189
+ },
190
+ signal: controller?.signal,
191
+ });
192
+ const responseMs = Math.max(0, __timeNowMs() - startedAtMs);
193
+ let payload = null;
194
+ try {
195
+ payload = await response.json();
196
+ } catch {
197
+ payload = null;
198
+ }
199
+
200
+ const version = sanitizeText(payload?.version || '', 40, { allowEmpty: true }) || null;
201
+ const commit = sanitizeText(payload?.commit || '', 80, { allowEmpty: true }) || null;
202
+ const database = sanitizeText(payload?.database || '', 30, { allowEmpty: true }) || null;
203
+ const normalizedDatabase = String(database || '')
204
+ .trim()
205
+ .toLowerCase();
206
+ const status = !response.ok ? 'offline' : normalizedDatabase === 'ok' ? 'online' : normalizedDatabase ? 'degraded' : 'online';
207
+
208
+ const snapshot = {
209
+ ...baseSnapshot,
210
+ available: response.ok,
211
+ status,
212
+ response_ms: responseMs,
213
+ checked_at: __timeNowIso(),
214
+ http_status: Number(response.status || 0) || null,
215
+ version,
216
+ database,
217
+ commit,
218
+ };
219
+ grafanaRuntimeSnapshotCache = {
220
+ expires_at_ms: __timeNowMs() + SYSTEM_ADMIN_GRAFANA_STATUS_CACHE_TTL_MS,
221
+ payload: snapshot,
222
+ };
223
+ return snapshot;
224
+ } catch (error) {
225
+ const responseMs = Math.max(0, __timeNowMs() - startedAtMs);
226
+ const isAbort = error?.name === 'AbortError';
227
+ const snapshot = {
228
+ ...baseSnapshot,
229
+ status: 'offline',
230
+ response_ms: responseMs,
231
+ checked_at: __timeNowIso(),
232
+ error: sanitizeText(isAbort ? 'timeout' : error?.message || 'fetch_failed', 80, { allowEmpty: true }) || 'fetch_failed',
233
+ };
234
+ grafanaRuntimeSnapshotCache = {
235
+ expires_at_ms: __timeNowMs() + SYSTEM_ADMIN_GRAFANA_STATUS_CACHE_TTL_MS,
236
+ payload: snapshot,
237
+ };
238
+ return snapshot;
239
+ } finally {
240
+ if (timeoutId) clearTimeout(timeoutId);
241
+ }
242
+ };
19
243
 
20
244
  const adminPanelSessionMap = new Map();
21
245
  let adminPanelSessionPruneAt = 0;
@@ -957,7 +1181,7 @@ export const createStickerCatalogAdminHandlers = ({ executeQuery, tables, logger
957
1181
  };
958
1182
 
959
1183
  const buildAdminOverviewPayload = async ({ adminSession = null } = {}) => {
960
- const [marketplaceStats, activeSessions, knownUsers, bans, packsCountRows, stickersCountRows, recentPacks, visitSummary, systemSummaryPayload, messageFlowDaily, moderationQueue, auditLog, featureFlags] = await Promise.all([
1184
+ const [marketplaceStats, activeSessions, knownUsers, bans, packsCountRows, stickersCountRows, recentPacks, visitSummary, systemSummaryPayload, messageFlowDaily, moderationQueue, auditLog, featureFlags, grafanaRuntimeSnapshot] = await Promise.all([
961
1185
  getMarketplaceGlobalStatsCached().catch(() => null),
962
1186
  listAdminActiveGoogleWebSessions({ limit: 80 }),
963
1187
  listAdminKnownGoogleUsers({ limit: 120 }),
@@ -976,6 +1200,19 @@ export const createStickerCatalogAdminHandlers = ({ executeQuery, tables, logger
976
1200
  buildModerationQueueSnapshot({ limit: 80 }).catch(() => []),
977
1201
  listAdminAuditLog({ limit: 120 }).catch(() => []),
978
1202
  listAdminFeatureFlagsDetailed({ limit: 300 }).catch(() => []),
1203
+ getGrafanaRuntimeSnapshot().catch(() => ({
1204
+ enabled: Boolean(grafanaObservabilityLinks?.enabled),
1205
+ available: false,
1206
+ status: 'offline',
1207
+ response_ms: null,
1208
+ checked_at: __timeNowIso(),
1209
+ http_status: null,
1210
+ version: null,
1211
+ database: null,
1212
+ commit: null,
1213
+ dashboards_total: Array.isArray(grafanaObservabilityLinks?.dashboards) ? grafanaObservabilityLinks.dashboards.length : 0,
1214
+ error: 'runtime_snapshot_failed',
1215
+ })),
979
1216
  ]);
980
1217
 
981
1218
  const systemSummary = systemSummaryPayload?.data || null;
@@ -997,7 +1234,7 @@ export const createStickerCatalogAdminHandlers = ({ executeQuery, tables, logger
997
1234
  systemMeta,
998
1235
  });
999
1236
 
1000
- return {
1237
+ const overviewPayload = {
1001
1238
  admin_session: mapAdminPanelSessionResponseData(adminSession),
1002
1239
  marketplace_stats: marketplaceStats,
1003
1240
  counters: {
@@ -1045,9 +1282,22 @@ export const createStickerCatalogAdminHandlers = ({ executeQuery, tables, logger
1045
1282
  visit_metrics: visitSummary,
1046
1283
  system_summary: systemSummary,
1047
1284
  system_meta: systemMeta,
1285
+ observability_links: {
1286
+ grafana: grafanaObservabilityLinks,
1287
+ },
1288
+ observability_runtime: {
1289
+ grafana: grafanaRuntimeSnapshot,
1290
+ },
1048
1291
  message_flow_daily: messageFlowDaily,
1049
1292
  updated_at: __timeNowIso(),
1050
1293
  };
1294
+
1295
+ setAdminOverviewSnapshot({
1296
+ overview: overviewPayload,
1297
+ source: 'admin_overview_payload',
1298
+ });
1299
+
1300
+ return overviewPayload;
1051
1301
  };
1052
1302
 
1053
1303
  const findAdminPackContextByKey = async (rawPackKey) => {
@@ -1,8 +1,17 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import { timingSafeEqual } from 'node:crypto';
3
4
  import { URL } from 'node:url';
4
5
 
5
6
  import logger from '#logger';
7
+ import {
8
+ forceSystemAdminGroupFailover,
9
+ listSystemAdminAssignmentHistory,
10
+ listSystemAdminAssignments,
11
+ listSystemAdminSessions,
12
+ setSystemAdminGroupPin,
13
+ triggerSystemAdminManualRebalance,
14
+ } from '../system/systemController.js';
6
15
 
7
16
  const parseEnvBool = (value, fallback) => {
8
17
  if (value === undefined || value === null || value === '') return fallback;
@@ -25,6 +34,8 @@ const SYSTEM_ADMIN_API_BASE_PATH = normalizeBasePath(process.env.SYSTEM_ADMIN_AP
25
34
  const SYSTEM_ADMIN_API_SESSION_PATH = `${SYSTEM_ADMIN_API_BASE_PATH}/session`;
26
35
  const LEGACY_SYSTEM_ADMIN_API_BASE_PATH = `${LEGACY_STICKER_API_BASE_PATH}/admin`;
27
36
  const LEGACY_SYSTEM_ADMIN_API_SESSION_PATH = `${LEGACY_SYSTEM_ADMIN_API_BASE_PATH}/session`;
37
+ const SYSTEM_ADMIN_MULTI_SESSION_API_PATH = `${SYSTEM_ADMIN_API_BASE_PATH}/multi-session`;
38
+ const LEGACY_SYSTEM_ADMIN_MULTI_SESSION_API_PATH = `${LEGACY_SYSTEM_ADMIN_API_BASE_PATH}/multi-session`;
28
39
  const STICKER_LOGIN_WEB_PATH = normalizeBasePath(process.env.STICKER_LOGIN_WEB_PATH, '/login');
29
40
  const STICKER_WEB_PATH = normalizeBasePath(process.env.STICKER_WEB_PATH, '/stickers');
30
41
  const STICKER_ADMIN_WEB_PATH = `${STICKER_WEB_PATH}/admin`;
@@ -37,6 +48,9 @@ const SITE_ORIGIN = String(process.env.SITE_ORIGIN || 'https://omnizap.shop')
37
48
 
38
49
  const USER_SYSTEMADM_TEMPLATE_PATH = path.join(process.cwd(), 'public', 'pages', 'user-systemadm.html');
39
50
  const LEGACY_STICKER_ADMIN_TEMPLATE_PATH = path.join(process.cwd(), 'public', 'pages', 'stickers-admin.html');
51
+ const SYSTEM_ADMIN_OPS_TOKEN = String(
52
+ process.env.SYSTEM_ADMIN_OPS_TOKEN || process.env.USER_INTERNAL_API_TOKEN || process.env.ADMIN_TOKEN || process.env.ADMIN_API_TOKEN || '',
53
+ ).trim();
40
54
 
41
55
  let stickerCatalogControllerPromise = null;
42
56
  const loadStickerCatalogController = async () => {
@@ -108,6 +122,106 @@ const mapAdminApiPathToLegacy = (pathname) => {
108
122
  return null;
109
123
  };
110
124
 
125
+ const mapMultiSessionApiPath = (pathname) => {
126
+ if (hasPathPrefix(pathname, SYSTEM_ADMIN_MULTI_SESSION_API_PATH)) {
127
+ const suffix = pathname.slice(SYSTEM_ADMIN_MULTI_SESSION_API_PATH.length);
128
+ return suffix || '/';
129
+ }
130
+ if (hasPathPrefix(pathname, LEGACY_SYSTEM_ADMIN_MULTI_SESSION_API_PATH)) {
131
+ const suffix = pathname.slice(LEGACY_SYSTEM_ADMIN_MULTI_SESSION_API_PATH.length);
132
+ return suffix || '/';
133
+ }
134
+ return null;
135
+ };
136
+
137
+ const constantTimeStringEqual = (left, right) => {
138
+ const leftBuffer = Buffer.from(String(left || ''), 'utf8');
139
+ const rightBuffer = Buffer.from(String(right || ''), 'utf8');
140
+ if (!leftBuffer.length || leftBuffer.length !== rightBuffer.length) return false;
141
+ try {
142
+ return timingSafeEqual(leftBuffer, rightBuffer);
143
+ } catch {
144
+ return false;
145
+ }
146
+ };
147
+
148
+ const extractBearerToken = (req) => {
149
+ const authHeader = String(req?.headers?.authorization || '').trim();
150
+ if (!authHeader.toLowerCase().startsWith('bearer ')) return '';
151
+ return authHeader.slice(7).trim();
152
+ };
153
+
154
+ const resolveOpsTokenFromRequest = (req) =>
155
+ String(req?.headers?.['x-system-admin-token'] || req?.headers?.['x-internal-api-token'] || req?.headers?.['x-admin-token'] || '').trim() ||
156
+ extractBearerToken(req);
157
+
158
+ const hasValidOpsToken = (req) => {
159
+ if (!SYSTEM_ADMIN_OPS_TOKEN) return true;
160
+ const requestToken = resolveOpsTokenFromRequest(req);
161
+ if (!requestToken) return false;
162
+ return constantTimeStringEqual(requestToken, SYSTEM_ADMIN_OPS_TOKEN);
163
+ };
164
+
165
+ const readJsonBody = async (req, { maxBytes = 64 * 1024 } = {}) =>
166
+ new Promise((resolve, reject) => {
167
+ const chunks = [];
168
+ let total = 0;
169
+
170
+ req.on('data', (chunk) => {
171
+ total += chunk.length;
172
+ if (total > maxBytes) {
173
+ const error = new Error('Payload excedeu limite permitido.');
174
+ error.statusCode = 413;
175
+ reject(error);
176
+ req.destroy();
177
+ return;
178
+ }
179
+ chunks.push(chunk);
180
+ });
181
+
182
+ req.on('end', () => {
183
+ const raw = Buffer.concat(chunks).toString('utf8').trim();
184
+ if (!raw) {
185
+ resolve({});
186
+ return;
187
+ }
188
+
189
+ try {
190
+ resolve(JSON.parse(raw));
191
+ } catch {
192
+ const error = new Error('JSON invalido.');
193
+ error.statusCode = 400;
194
+ reject(error);
195
+ }
196
+ });
197
+
198
+ req.on('error', (error) => reject(error));
199
+ });
200
+
201
+ const parseBool = (value, fallback = false) => {
202
+ if (value === undefined || value === null || value === '') return fallback;
203
+ const normalized = String(value).trim().toLowerCase();
204
+ if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
205
+ if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
206
+ return fallback;
207
+ };
208
+
209
+ const parsePositiveInt = (value, fallback = 200, min = 1, max = 5000) => {
210
+ const parsed = Number.parseInt(String(value ?? ''), 10);
211
+ if (!Number.isFinite(parsed)) return fallback;
212
+ return Math.max(min, Math.min(max, parsed));
213
+ };
214
+
215
+ const decodePathSegment = (value) => {
216
+ const raw = String(value || '').trim();
217
+ if (!raw) return '';
218
+ try {
219
+ return decodeURIComponent(raw);
220
+ } catch {
221
+ return raw;
222
+ }
223
+ };
224
+
111
225
  const renderUserSystemAdminHtml = async () => {
112
226
  const template = await fs.readFile(USER_SYSTEMADM_TEMPLATE_PATH, 'utf8');
113
227
  const dataAttributes = {
@@ -124,13 +238,147 @@ const renderUserSystemAdminHtml = async () => {
124
238
  return html;
125
239
  };
126
240
 
241
+ const requireSystemAdminOpsAccess = (req, res) => {
242
+ if (hasValidOpsToken(req)) return true;
243
+ sendJson(req, res, 401, { error: 'Nao autorizado para operacoes de system admin.' });
244
+ return false;
245
+ };
246
+
247
+ const normalizeMultiSessionSubPath = (value) => {
248
+ const raw = String(value || '/').trim();
249
+ if (!raw || raw === '/') return '/';
250
+ return `/${raw
251
+ .replace(/^\/+/g, '')
252
+ .replace(/\/+$/g, '')}`;
253
+ };
254
+
255
+ const handleMultiSessionOpsRequest = async (req, res, { pathname, url }) => {
256
+ if (!requireSystemAdminOpsAccess(req, res)) return true;
257
+
258
+ const subPath = normalizeMultiSessionSubPath(pathname);
259
+ const requestUrl = (() => {
260
+ try {
261
+ return new URL(String(url?.href || req.url || '/'), SITE_ORIGIN);
262
+ } catch {
263
+ return new URL(SITE_ORIGIN);
264
+ }
265
+ })();
266
+
267
+ if (subPath === '/sessions') {
268
+ if (!['GET', 'HEAD'].includes(req.method || '')) {
269
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
270
+ return true;
271
+ }
272
+ const payload = await listSystemAdminSessions({
273
+ status: requestUrl.searchParams.get('status'),
274
+ limit: parsePositiveInt(requestUrl.searchParams.get('limit'), 200, 1, 5000),
275
+ });
276
+ sendJson(req, res, 200, payload);
277
+ return true;
278
+ }
279
+
280
+ if (subPath === '/assignments') {
281
+ if (!['GET', 'HEAD'].includes(req.method || '')) {
282
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
283
+ return true;
284
+ }
285
+ const payload = await listSystemAdminAssignments({
286
+ groupJid: requestUrl.searchParams.get('group_jid'),
287
+ ownerSessionId: requestUrl.searchParams.get('owner_session_id'),
288
+ includeExpired: parseBool(requestUrl.searchParams.get('include_expired'), false),
289
+ limit: parsePositiveInt(requestUrl.searchParams.get('limit'), 200, 1, 5000),
290
+ });
291
+ sendJson(req, res, 200, payload);
292
+ return true;
293
+ }
294
+
295
+ if (subPath === '/history') {
296
+ if (!['GET', 'HEAD'].includes(req.method || '')) {
297
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
298
+ return true;
299
+ }
300
+ const payload = await listSystemAdminAssignmentHistory({
301
+ groupJid: requestUrl.searchParams.get('group_jid'),
302
+ limit: parsePositiveInt(requestUrl.searchParams.get('limit'), 100, 1, 5000),
303
+ });
304
+ sendJson(req, res, 200, payload);
305
+ return true;
306
+ }
307
+
308
+ if (subPath === '/rebalance') {
309
+ if (!['POST'].includes(req.method || '')) {
310
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
311
+ return true;
312
+ }
313
+ const payload = await triggerSystemAdminManualRebalance();
314
+ sendJson(req, res, 200, payload);
315
+ return true;
316
+ }
317
+
318
+ const groupActionMatch = subPath.match(/^\/groups\/([^/]+)\/(pin|unpin|failover)$/i);
319
+ if (groupActionMatch) {
320
+ if (!['POST'].includes(req.method || '')) {
321
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
322
+ return true;
323
+ }
324
+
325
+ const groupJid = decodePathSegment(groupActionMatch[1]);
326
+ const action = String(groupActionMatch[2] || '').toLowerCase();
327
+ const body = await readJsonBody(req).catch((error) => {
328
+ const statusCode = Number(error?.statusCode || 400);
329
+ sendJson(req, res, statusCode, { error: error?.message || 'Falha ao interpretar payload JSON.' });
330
+ return null;
331
+ });
332
+ if (body === null) return true;
333
+
334
+ if (action === 'pin' || action === 'unpin') {
335
+ const pinned = action === 'pin';
336
+ const payload = await setSystemAdminGroupPin({
337
+ groupJid,
338
+ pinned,
339
+ sessionId: body?.session_id || body?.sessionId || null,
340
+ reason: body?.reason || null,
341
+ changedBy: 'system_admin_api',
342
+ metadata: body?.metadata || null,
343
+ });
344
+ sendJson(req, res, 200, payload);
345
+ return true;
346
+ }
347
+
348
+ if (action === 'failover') {
349
+ const targetSessionId = String(body?.target_session_id || body?.targetSessionId || requestUrl.searchParams.get('target_session_id') || '')
350
+ .trim()
351
+ .slice(0, 64);
352
+ if (!targetSessionId) {
353
+ sendJson(req, res, 400, { error: 'target_session_id e obrigatorio.' });
354
+ return true;
355
+ }
356
+
357
+ const payload = await forceSystemAdminGroupFailover({
358
+ groupJid,
359
+ targetSessionId,
360
+ reason: body?.reason || 'admin_force_failover',
361
+ changedBy: 'system_admin_api',
362
+ metadata: body?.metadata || null,
363
+ });
364
+ sendJson(req, res, 200, payload);
365
+ return true;
366
+ }
367
+ }
368
+
369
+ sendJson(req, res, 404, { error: 'Endpoint de operacao multi-session nao encontrado.' });
370
+ return true;
371
+ };
372
+
127
373
  export const getSystemAdminRouteConfig = () => ({
128
374
  webPath: USER_SYSTEMADM_WEB_PATH,
129
375
  legacyWebPath: STICKER_ADMIN_WEB_PATH,
130
376
  apiAdminBasePath: SYSTEM_ADMIN_API_BASE_PATH,
131
377
  apiAdminSessionPath: SYSTEM_ADMIN_API_SESSION_PATH,
378
+ apiAdminMultiSessionPath: SYSTEM_ADMIN_MULTI_SESSION_API_PATH,
132
379
  legacyApiAdminBasePath: LEGACY_SYSTEM_ADMIN_API_BASE_PATH,
133
380
  legacyApiAdminSessionPath: LEGACY_SYSTEM_ADMIN_API_SESSION_PATH,
381
+ legacyApiAdminMultiSessionPath: LEGACY_SYSTEM_ADMIN_MULTI_SESSION_API_PATH,
134
382
  });
135
383
 
136
384
  export const maybeHandleSystemAdminRequest = async (req, res, { pathname, url }) => {
@@ -186,6 +434,25 @@ export const maybeHandleSystemAdminRequest = async (req, res, { pathname, url })
186
434
  }
187
435
 
188
436
  if (hasPathPrefix(pathname, SYSTEM_ADMIN_API_BASE_PATH) || hasPathPrefix(pathname, LEGACY_SYSTEM_ADMIN_API_BASE_PATH)) {
437
+ const multiSessionPath = mapMultiSessionApiPath(pathname);
438
+ if (multiSessionPath !== null) {
439
+ try {
440
+ return await handleMultiSessionOpsRequest(req, res, {
441
+ pathname: multiSessionPath,
442
+ url,
443
+ });
444
+ } catch (error) {
445
+ logger.error('Falha ao processar endpoint operacional multi-sessao.', {
446
+ action: 'system_admin_multi_session_endpoint_failed',
447
+ method: req.method,
448
+ path: pathname,
449
+ error: error?.message,
450
+ });
451
+ sendJson(req, res, 500, { error: 'Falha interna ao processar operacao multi-sessao.' });
452
+ return true;
453
+ }
454
+ }
455
+
189
456
  const legacyPathname = mapAdminApiPathToLegacy(pathname);
190
457
  if (!legacyPathname) return false;
191
458
 
@@ -67,6 +67,7 @@ export const stripWebpExtension = (value) =>
67
67
  .replace(/\.webp$/i, '');
68
68
 
69
69
  const clampInt = (value, fallback, min, max) => {
70
+ if (value === undefined || value === null || value === '') return fallback;
70
71
  const parsed = Number(value);
71
72
  if (!Number.isFinite(parsed)) return fallback;
72
73
  return Math.max(min, Math.min(max, Math.floor(parsed)));
@@ -964,8 +965,6 @@ const convertUploadMediaToWebp = async ({ ownerJid, buffer, mimetype }) => {
964
965
  const PACK_TAG_MARKER_REGEX = /\[pack-tags:([^\]]+)\]/i;
965
966
  const AUTO_PACK_MARKER_REGEX = /\[(?:auto-theme|auto-tag):[^\]]+\]/gi;
966
967
  const AUTO_PACK_MARKER_TEST_REGEX = /\[(?:auto-theme|auto-tag):[^\]]+\]/i;
967
- const AUTO_PACK_COLLECTOR_MARKER = '[auto-pack:collector]';
968
- const AUTO_PACK_COLLECTOR_LEGACY_TEXT = 'coleção automática de figurinhas criadas pelo usuário.';
969
968
  const AUTO_PACK_DESCRIPTION_PREFIX_REGEX = /^curadoria automática por tema\.\s*tema:\s*[^.]+\.?\s*(?:score\s*=\s*-?\d+(?:\.\d+)?\.?\s*)?/i;
970
969
  const AUTO_PACK_SCORE_FRAGMENT_REGEX = /\bscore\s*=\s*-?\d+(?:\.\d+)?\.?/gi;
971
970
  const normalizePackTag = (value) =>
@@ -1024,29 +1023,10 @@ const parsePackDescriptionMetadata = (description) => {
1024
1023
  };
1025
1024
  };
1026
1025
 
1027
- const isCollectorAutoPack = (pack) => {
1028
- if (!pack || typeof pack !== 'object') return false;
1029
- const description = String(pack.description || '').toLowerCase();
1030
- return description.includes(AUTO_PACK_COLLECTOR_MARKER) || description.includes(AUTO_PACK_COLLECTOR_LEGACY_TEXT);
1031
- };
1032
-
1033
- const isThemeCurationAutoPack = (pack) => {
1034
- if (!pack || typeof pack !== 'object') return false;
1035
- const name = String(pack.name || '').trim();
1036
- if (/^\[auto\]/i.test(name)) return true;
1037
-
1038
- const description = String(pack.description || '').toLowerCase();
1039
- if (description.includes('[auto-theme:') || description.includes('[auto-tag:')) return true;
1040
-
1041
- return Boolean(String(pack.pack_theme_key || '').trim());
1042
- };
1043
-
1044
1026
  const shouldHidePackFromMyProfileDefault = (pack, { includeAutoPacks = false } = {}) => {
1045
1027
  if (!pack || typeof pack !== 'object') return false;
1046
1028
  if (includeAutoPacks) return false;
1047
- if (isCollectorAutoPack(pack)) return false;
1048
- if (isThemeCurationAutoPack(pack)) return true;
1049
- return pack.is_auto_pack === true || Number(pack.is_auto_pack || 0) === 1;
1029
+ return false;
1050
1030
  };
1051
1031
 
1052
1032
  const buildPackDescriptionWithTags = (description, tags = []) => {
@@ -1734,6 +1714,12 @@ const getMarketplaceStatsCached = async (visibility) => {
1734
1714
  bucket.expiresAt = __timeNowMs() + HOME_MARKETPLACE_STATS_CACHE_SECONDS * 1000;
1735
1715
  return data;
1736
1716
  })
1717
+ .catch((error) => {
1718
+ if (hasValue && bucket.value) {
1719
+ return bucket.value;
1720
+ }
1721
+ throw error;
1722
+ })
1737
1723
  .finally(() => {
1738
1724
  bucket.pending = null;
1739
1725
  });
@@ -3219,7 +3205,7 @@ const handleCreatePackRequest = async (req, res) => {
3219
3205
  });
3220
3206
  const manualTags = mergeUniqueTags(Array.isArray(payload?.tags) ? payload.tags : []).slice(0, 8);
3221
3207
  const persistedDescription = buildPackDescriptionWithTags(description, manualTags);
3222
- const visibility = String(payload?.visibility || 'public')
3208
+ const visibility = String(payload?.visibility || 'private')
3223
3209
  .trim()
3224
3210
  .toLowerCase();
3225
3211
  const googleSession = await resolveGoogleWebSessionFromRequest(req);