@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,13 +1,12 @@
1
1
  import logger from '#logger';
2
2
  import { getActiveSocket, getAdminPhone, getAdminRawValue, getJidUser, resolveAdminJid, resolveBotJid, extractUserIdInfo, resolveUserId } from '../../../app/config/index.js';
3
+ import { isLikelyWhatsAppPhone, normalizePhoneDigits, resolveAdminPhoneFromEnv, resolveBotPhoneFromEnv, resolveSupportPhoneFromEnv } from '../../../utils/whatsapp/contactEnv.js';
3
4
 
4
5
  const PACK_COMMAND_PREFIX = String(process.env.COMMAND_PREFIX || '/').trim() || '/';
5
6
 
6
- const normalizePhoneDigits = (value) => String(value || '').replace(/\D+/g, '');
7
-
8
7
  const isPlausibleWhatsAppPhone = (value) => {
9
8
  const digits = normalizePhoneDigits(value);
10
- return digits.length >= 10 && digits.length <= 15 ? digits : '';
9
+ return isLikelyWhatsAppPhone(digits) ? digits : '';
11
10
  };
12
11
 
13
12
  const resolveActiveSocketBotJid = (activeSocket) => {
@@ -26,18 +25,12 @@ export const resolveCatalogBotPhone = () => {
26
25
  const jidUser = botJid ? getJidUser(botJid) : null;
27
26
  const fromSocket = normalizePhoneDigits(jidUser || '');
28
27
 
29
- if (fromSocket && fromSocket.length >= 10) {
28
+ if (isLikelyWhatsAppPhone(fromSocket)) {
30
29
  return fromSocket;
31
30
  }
32
31
 
33
- const envCandidates = [process.env.WHATSAPP_BOT_NUMBER, process.env.BOT_NUMBER, process.env.PHONE_NUMBER, process.env.BOT_PHONE_NUMBER, process.env.USER_ADMIN];
34
-
35
- for (const candidate of envCandidates) {
36
- const digits = normalizePhoneDigits(candidate || '');
37
- if (digits && digits.length >= 10 && digits.length <= 15) {
38
- return digits;
39
- }
40
- }
32
+ const fromEnv = resolveBotPhoneFromEnv({ fallback: '' });
33
+ if (fromEnv) return fromEnv;
41
34
 
42
35
  logger.warn('Nao foi possivel resolver o numero do bot para contato.', {
43
36
  action: 'resolve_bot_phone_failed',
@@ -75,12 +68,11 @@ const resolveSupportAdminPhone = async () => {
75
68
  const adminPhone = isPlausibleWhatsAppPhone(getAdminPhone() || '');
76
69
  if (adminPhone) return adminPhone;
77
70
 
78
- const candidates = [process.env.WHATSAPP_SUPPORT_NUMBER, process.env.OWNER_NUMBER, process.env.USER_ADMIN];
71
+ const configuredAdminPhone = resolveAdminPhoneFromEnv({ fallback: '' });
72
+ if (configuredAdminPhone) return configuredAdminPhone;
79
73
 
80
- for (const candidate of candidates) {
81
- const digits = isPlausibleWhatsAppPhone(getJidUser(candidate || '') || candidate);
82
- if (digits) return digits;
83
- }
74
+ const configuredSupportPhone = resolveSupportPhoneFromEnv({ fallback: '' });
75
+ if (configuredSupportPhone) return configuredSupportPhone;
84
76
 
85
77
  return '';
86
78
  };
@@ -1,5 +1,6 @@
1
1
  import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
2
2
  import { withTimeout } from '../../http/httpRequestUtils.js';
3
+ import { resolveBotPhoneFromEnv } from '../../../utils/whatsapp/contactEnv.js';
3
4
 
4
5
  export const createStickerCatalogSystemContext = ({ executeQuery, tables, logger, getSystemMetrics, getActiveSocket, resolveSocketReadyState, resolveActiveSocketBotJid, resolveCatalogBotPhone, fetchPrometheusSummary, metricsEndpoint, systemSummaryCache, systemSummaryCacheSeconds, readmeSummaryCache, readmeSummaryCacheSeconds, readmeMessageTypeSampleLimit, readmeCommandPrefix, buildMenuCaption, buildStickerMenu, buildMediaMenu, buildQuoteMenu, buildAnimeMenu, buildAiMenu, buildStatsMenu, buildAdminMenu, profilePictureUrlFromActiveSocket, normalizeJid, getJidUser, globalRankCache, globalRankRefreshSeconds, marketplaceGlobalStatsCache, marketplaceGlobalStatsCacheSeconds }) => {
5
6
  let globalRankRefreshTimer = null;
@@ -153,6 +154,12 @@ export const createStickerCatalogSystemContext = ({ executeQuery, tables, logger
153
154
  systemSummaryCache.expiresAt = __timeNowMs() + systemSummaryCacheSeconds * 1000;
154
155
  return payload;
155
156
  })
157
+ .catch((error) => {
158
+ if (hasValue && systemSummaryCache.value) {
159
+ return systemSummaryCache.value;
160
+ }
161
+ throw error;
162
+ })
156
163
  .finally(() => {
157
164
  systemSummaryCache.pending = null;
158
165
  });
@@ -329,6 +336,12 @@ export const createStickerCatalogSystemContext = ({ executeQuery, tables, logger
329
336
  readmeSummaryCache.expiresAt = __timeNowMs() + readmeSummaryCacheSeconds * 1000;
330
337
  return payload;
331
338
  })
339
+ .catch((error) => {
340
+ if (hasValue && readmeSummaryCache.value) {
341
+ return readmeSummaryCache.value;
342
+ }
343
+ throw error;
344
+ })
332
345
  .finally(() => {
333
346
  readmeSummaryCache.pending = null;
334
347
  });
@@ -346,12 +359,8 @@ export const createStickerCatalogSystemContext = ({ executeQuery, tables, logger
346
359
  const botPhoneFromCatalog = String(resolveCatalogBotPhone() || '').replace(/\D+/g, '');
347
360
  if (botPhoneFromCatalog) candidates.add(botPhoneFromCatalog);
348
361
 
349
- const envCandidates = [process.env.WHATSAPP_BOT_NUMBER, process.env.BOT_NUMBER, process.env.PHONE_NUMBER, process.env.BOT_PHONE_NUMBER];
350
-
351
- for (const candidate of envCandidates) {
352
- const digits = String(candidate || '').replace(/\D+/g, '');
353
- if (digits) candidates.add(digits);
354
- }
362
+ const configuredBotPhone = String(resolveBotPhoneFromEnv({ fallback: '' }) || '').replace(/\D+/g, '');
363
+ if (configuredBotPhone) candidates.add(configuredBotPhone);
355
364
 
356
365
  return Array.from(candidates).filter((value) => value.length >= 8);
357
366
  };
@@ -542,6 +551,12 @@ export const createStickerCatalogSystemContext = ({ executeQuery, tables, logger
542
551
  globalRankCache.expiresAt = __timeNowMs() + globalRankRefreshSeconds * 1000;
543
552
  return data;
544
553
  })
554
+ .catch((error) => {
555
+ if (hasValue && globalRankCache.value) {
556
+ return globalRankCache.value;
557
+ }
558
+ throw error;
559
+ })
545
560
  .finally(() => {
546
561
  globalRankCache.pending = null;
547
562
  });
@@ -737,6 +752,12 @@ export const createStickerCatalogSystemContext = ({ executeQuery, tables, logger
737
752
  marketplaceGlobalStatsCache.expiresAt = __timeNowMs() + marketplaceGlobalStatsCacheSeconds * 1000;
738
753
  return data;
739
754
  })
755
+ .catch((error) => {
756
+ if (hasValue && marketplaceGlobalStatsCache.value) {
757
+ return marketplaceGlobalStatsCache.value;
758
+ }
759
+ throw error;
760
+ })
740
761
  .finally(() => {
741
762
  marketplaceGlobalStatsCache.pending = null;
742
763
  });
@@ -1,6 +1,6 @@
1
1
  import logger from '#logger';
2
2
  import { executeQuery, TABLES } from '../../../database/index.js';
3
- import { getActiveSocket, getJidUser, normalizeJid, profilePictureUrlFromActiveSocket } from '../../../app/config/index.js';
3
+ import { getActiveSocket, getActiveSocketsBySession, getJidUser, getMultiSessionRuntimeConfig, isSocketOpen, normalizeJid, profilePictureUrlFromActiveSocket } from '../../../app/config/index.js';
4
4
  import { getSystemMetrics } from '../../../app/utils/systemMetrics/systemMetricsModule.js';
5
5
  import { createStickerCatalogSystemContext } from './stickerCatalogSystemContext.js';
6
6
  import { createStickerCatalogNonCatalogHandlers } from '../sticker/nonCatalogHandlers.js';
@@ -10,6 +10,9 @@ import { fetchPrometheusSummary } from './systemMetricsController.js';
10
10
  import { buildBotContactInfo, buildSupportInfo, resolveCatalogBotPhone } from './contactController.js';
11
11
  import { buildAdminMenu, buildAiMenu, buildAnimeMenu, buildMediaMenu, buildMenuCaption, buildQuoteMenu, buildStatsMenu, buildStickerMenu } from '../../../app/modules/menuModule/common.js';
12
12
  import { trackWebVisitMetric } from './visitController.js';
13
+ import groupOwnershipService from '../../../app/services/multiSession/groupOwnershipService.js';
14
+ import sessionRegistryService from '../../../app/services/multiSession/sessionRegistryService.js';
15
+ import { runGroupAssignmentBalancerCycle } from '../../../app/services/multiSession/assignmentBalancerService.js';
13
16
 
14
17
  const SYSTEM_SUMMARY_CACHE_SECONDS = Number(process.env.SYSTEM_SUMMARY_CACHE_SECONDS || 20);
15
18
  const README_SUMMARY_CACHE_SECONDS = Number(process.env.README_SUMMARY_CACHE_SECONDS || 1800);
@@ -132,4 +135,254 @@ export const systemHandlers = createStickerCatalogNonCatalogHandlers({
132
135
  isAuthenticatedGoogleSession: (sess) => Boolean(sess?.sub && (sess?.ownerJid || sess?.ownerPhone || sess?.email)),
133
136
  });
134
137
 
138
+ const clampLimit = (value, fallback = 200, min = 1, max = 5_000) => {
139
+ const parsed = Number.parseInt(String(value ?? ''), 10);
140
+ if (!Number.isFinite(parsed)) return fallback;
141
+ return Math.max(min, Math.min(max, parsed));
142
+ };
143
+
144
+ const normalizeOptional = (value, maxLength = 255) => {
145
+ const normalized = String(value || '')
146
+ .trim()
147
+ .slice(0, maxLength);
148
+ return normalized || null;
149
+ };
150
+
151
+ const normalizeBoolean = (value, fallback = false) => {
152
+ if (value === undefined || value === null || value === '') return fallback;
153
+ const normalized = String(value).trim().toLowerCase();
154
+ if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
155
+ if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
156
+ return fallback;
157
+ };
158
+
159
+ const toIso = (value) => {
160
+ if (!value) return null;
161
+ const parsed = new Date(value);
162
+ if (Number.isNaN(parsed.getTime())) return null;
163
+ return parsed.toISOString();
164
+ };
165
+
166
+ const resolveSocketRuntimeState = (socket) => {
167
+ const open = isSocketOpen(socket);
168
+ const readyState = resolveSocketReadyState(socket);
169
+ const botJid = resolveActiveSocketBotJid(socket);
170
+ return {
171
+ socket_open: open,
172
+ socket_ready_state: readyState,
173
+ socket_bot_jid: botJid || null,
174
+ };
175
+ };
176
+
177
+ export const listSystemAdminSessions = async ({ status = null, limit = 200 } = {}) => {
178
+ const runtimeConfig = getMultiSessionRuntimeConfig();
179
+ const safeStatus = normalizeOptional(status, 24);
180
+ const safeLimit = clampLimit(limit, 200, 1, 5_000);
181
+ const registryRows = await sessionRegistryService.listSessions({
182
+ status: safeStatus,
183
+ limit: safeLimit,
184
+ });
185
+
186
+ const socketsBySession = getActiveSocketsBySession();
187
+ const knownSessionIds = new Set();
188
+ for (const sessionId of runtimeConfig.sessionIds || []) knownSessionIds.add(sessionId);
189
+ for (const row of registryRows || []) {
190
+ if (row?.sessionId) knownSessionIds.add(row.sessionId);
191
+ }
192
+
193
+ const selectedSessionIds = Array.from(knownSessionIds)
194
+ .filter(Boolean)
195
+ .slice(0, safeLimit);
196
+ const registryBySession = new Map((registryRows || []).map((row) => [row.sessionId, row]));
197
+
198
+ const sessions = selectedSessionIds.map((sessionId) => {
199
+ const row = registryBySession.get(sessionId) || null;
200
+ const socket = socketsBySession.get(sessionId) || null;
201
+ const socketRuntime = resolveSocketRuntimeState(socket);
202
+
203
+ return {
204
+ session_id: sessionId,
205
+ is_primary: sessionId === runtimeConfig.primarySessionId,
206
+ configured: (runtimeConfig.sessionIds || []).includes(sessionId),
207
+ configured_weight: Number(runtimeConfig.sessionWeights?.[sessionId] || 1),
208
+ status: row?.status || (socketRuntime.socket_open ? 'online' : 'offline'),
209
+ bot_jid: row?.botJid || socketRuntime.socket_bot_jid || null,
210
+ capacity_weight: Number(row?.capacityWeight || runtimeConfig.sessionWeights?.[sessionId] || 1),
211
+ current_score: Number(row?.currentScore || 0),
212
+ last_heartbeat_at: toIso(row?.lastHeartbeatAt),
213
+ last_connected_at: toIso(row?.lastConnectedAt),
214
+ last_disconnected_at: toIso(row?.lastDisconnectedAt),
215
+ updated_at: toIso(row?.updatedAt),
216
+ metadata: row?.metadata || null,
217
+ ...socketRuntime,
218
+ };
219
+ });
220
+
221
+ return {
222
+ generated_at: new Date().toISOString(),
223
+ primary_session_id: runtimeConfig.primarySessionId,
224
+ configured_session_ids: runtimeConfig.sessionIds || [],
225
+ sessions,
226
+ };
227
+ };
228
+
229
+ export const listSystemAdminAssignments = async (
230
+ {
231
+ groupJid = null,
232
+ ownerSessionId = null,
233
+ includeExpired = false,
234
+ limit = 200,
235
+ } = {},
236
+ ) => {
237
+ const safeGroupJid = normalizeOptional(groupJid, 255);
238
+ const safeOwnerSessionId = normalizeOptional(ownerSessionId, 64);
239
+ const safeIncludeExpired = normalizeBoolean(includeExpired, false);
240
+ const safeLimit = clampLimit(limit, 200, 1, 5_000);
241
+
242
+ const assignments = await groupOwnershipService.listAssignments({
243
+ groupJid: safeGroupJid,
244
+ ownerSessionId: safeOwnerSessionId,
245
+ includeExpired: safeIncludeExpired,
246
+ limit: safeLimit,
247
+ });
248
+
249
+ return {
250
+ generated_at: new Date().toISOString(),
251
+ filters: {
252
+ group_jid: safeGroupJid,
253
+ owner_session_id: safeOwnerSessionId,
254
+ include_expired: safeIncludeExpired,
255
+ limit: safeLimit,
256
+ },
257
+ assignments: (assignments || []).map((assignment) => ({
258
+ group_jid: assignment?.groupJid || null,
259
+ owner_session_id: assignment?.ownerSessionId || null,
260
+ lease_expires_at: toIso(assignment?.leaseExpiresAt),
261
+ cooldown_until: toIso(assignment?.cooldownUntil),
262
+ assignment_version: Number(assignment?.assignmentVersion || 1),
263
+ pinned: assignment?.pinned === true,
264
+ active: assignment?.active !== false,
265
+ last_reason: assignment?.lastReason || null,
266
+ created_at: toIso(assignment?.createdAt),
267
+ updated_at: toIso(assignment?.updatedAt),
268
+ })),
269
+ };
270
+ };
271
+
272
+ export const setSystemAdminGroupPin = async (
273
+ {
274
+ groupJid,
275
+ pinned,
276
+ sessionId = null,
277
+ reason = null,
278
+ changedBy = 'admin_api',
279
+ metadata = null,
280
+ } = {},
281
+ ) => {
282
+ const outcome = await groupOwnershipService.setPinned({
283
+ groupJid,
284
+ pinned,
285
+ sessionId,
286
+ reason: reason || (pinned ? 'admin_pin_group' : 'admin_unpin_group'),
287
+ changedBy,
288
+ metadata,
289
+ });
290
+
291
+ return {
292
+ updated: Boolean(outcome?.updated),
293
+ reason: outcome?.reason || null,
294
+ assignment_version: Number(outcome?.assignmentVersion || 0) || null,
295
+ previous_owner_session_id: outcome?.previousOwnerSessionId || null,
296
+ owner: outcome?.owner
297
+ ? {
298
+ group_jid: outcome.owner.groupJid,
299
+ owner_session_id: outcome.owner.ownerSessionId,
300
+ lease_expires_at: toIso(outcome.owner.leaseExpiresAt),
301
+ cooldown_until: toIso(outcome.owner.cooldownUntil),
302
+ assignment_version: Number(outcome.owner.assignmentVersion || 1),
303
+ pinned: outcome.owner.pinned === true,
304
+ last_reason: outcome.owner.lastReason || null,
305
+ }
306
+ : null,
307
+ };
308
+ };
309
+
310
+ export const forceSystemAdminGroupFailover = async (
311
+ {
312
+ groupJid,
313
+ targetSessionId,
314
+ reason = 'admin_force_failover',
315
+ changedBy = 'admin_api',
316
+ metadata = null,
317
+ } = {},
318
+ ) => {
319
+ const outcome = await groupOwnershipService.forceAssign({
320
+ groupJid,
321
+ sessionId: targetSessionId,
322
+ reason,
323
+ changedBy,
324
+ metadata,
325
+ });
326
+
327
+ return {
328
+ reassigned: Boolean(outcome?.reassigned),
329
+ reason: outcome?.reason || null,
330
+ assignment_version: Number(outcome?.assignmentVersion || 0) || null,
331
+ previous_owner_session_id: outcome?.previousOwnerSessionId || null,
332
+ owner: outcome?.owner
333
+ ? {
334
+ group_jid: outcome.owner.groupJid,
335
+ owner_session_id: outcome.owner.ownerSessionId,
336
+ lease_expires_at: toIso(outcome.owner.leaseExpiresAt),
337
+ assignment_version: Number(outcome.owner.assignmentVersion || 1),
338
+ pinned: outcome.owner.pinned === true,
339
+ last_reason: outcome.owner.lastReason || null,
340
+ }
341
+ : null,
342
+ };
343
+ };
344
+
345
+ export const triggerSystemAdminManualRebalance = async () => {
346
+ const cycle = await runGroupAssignmentBalancerCycle();
347
+ return {
348
+ generated_at: new Date().toISOString(),
349
+ cycle,
350
+ };
351
+ };
352
+
353
+ export const listSystemAdminAssignmentHistory = async ({ groupJid = null, limit = 100 } = {}) => {
354
+ const safeLimit = clampLimit(limit, 100, 1, 5_000);
355
+ const safeGroupJid = normalizeOptional(groupJid, 255);
356
+ const params = [];
357
+ const where = [];
358
+ if (safeGroupJid) {
359
+ where.push('group_jid = ?');
360
+ params.push(safeGroupJid);
361
+ }
362
+
363
+ const rows = await executeQuery(
364
+ `SELECT id, group_jid, previous_session_id, new_session_id, change_reason, changed_by, assignment_version, metadata, created_at
365
+ FROM ${TABLES.GROUP_ASSIGNMENT_HISTORY}
366
+ ${where.length > 0 ? `WHERE ${where.join(' AND ')}` : ''}
367
+ ORDER BY id DESC
368
+ LIMIT ${safeLimit}`,
369
+ params,
370
+ );
371
+
372
+ return {
373
+ generated_at: new Date().toISOString(),
374
+ history: (Array.isArray(rows) ? rows : []).map((row) => ({
375
+ id: Number(row?.id || 0),
376
+ group_jid: row?.group_jid || null,
377
+ previous_session_id: row?.previous_session_id || null,
378
+ new_session_id: row?.new_session_id || null,
379
+ change_reason: row?.change_reason || null,
380
+ changed_by: row?.changed_by || null,
381
+ assignment_version: Number(row?.assignment_version || 0) || null,
382
+ metadata: row?.metadata || null,
383
+ created_at: toIso(row?.created_at),
384
+ })),
385
+ };
386
+ };
387
+
135
388
  export { scheduleGlobalRankingPreload };
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
 
4
4
  import logger from '#logger';
5
5
  import { DEFAULT_LEGACY_STICKER_API_BASE_PATH, DEFAULT_USER_API_BASE_PATH, isUserApiPath, normalizeBasePath, resolveLegacyUserApiPath } from '../routes/user/userApiPaths.js';
6
+ import { buildWhatsappUrl, resolvePublicWhatsappNumber } from '../utils/publicContact.js';
6
7
 
7
8
  const LEGACY_STICKER_API_BASE_PATH = normalizeBasePath(process.env.STICKER_API_BASE_PATH, DEFAULT_LEGACY_STICKER_API_BASE_PATH);
8
9
  const USER_API_BASE_PATH = normalizeBasePath(process.env.USER_API_BASE_PATH || process.env.AUTH_API_BASE_PATH, DEFAULT_USER_API_BASE_PATH);
@@ -11,6 +12,8 @@ const USER_PROFILE_WEB_PATH = normalizeBasePath(process.env.USER_PROFILE_WEB_PAT
11
12
  const USER_PASSWORD_RESET_WEB_PATH = normalizeBasePath(process.env.USER_PASSWORD_RESET_WEB_PATH, '/user/password-reset');
12
13
  const USER_DASHBOARD_TEMPLATE_PATH = path.join(process.cwd(), 'public', 'pages', 'user.html');
13
14
  const USER_PASSWORD_RESET_TEMPLATE_PATH = path.join(process.cwd(), 'public', 'pages', 'user-password-reset.html');
15
+ const PUBLIC_WHATSAPP_NUMBER = resolvePublicWhatsappNumber();
16
+ const PUBLIC_WHATSAPP_URL = buildWhatsappUrl(PUBLIC_WHATSAPP_NUMBER);
14
17
 
15
18
  const hasPathPrefix = (pathname, prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`);
16
19
  const escapeHtmlAttribute = (value) =>
@@ -52,7 +55,10 @@ const renderUserDashboardHtml = async ({ passwordReset = false } = {}) => {
52
55
  const dataAttributes = {
53
56
  'data-api-base-path': USER_API_BASE_PATH,
54
57
  'data-login-path': STICKER_LOGIN_WEB_PATH,
58
+ 'data-panel-path': USER_PROFILE_WEB_PATH,
55
59
  'data-password-reset-web-path': USER_PASSWORD_RESET_WEB_PATH,
60
+ 'data-support-whatsapp-number': PUBLIC_WHATSAPP_NUMBER,
61
+ 'data-support-whatsapp-url': PUBLIC_WHATSAPP_URL,
56
62
  };
57
63
 
58
64
  let html = template;
@@ -1,4 +1,5 @@
1
1
  import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
2
+ import { resolveAdminPhoneFromEnv, resolveBotPhoneFromEnv, resolveSupportPhoneFromEnv } from '../../utils/whatsapp/contactEnv.js';
2
3
  const DEFAULT_SITE_ORIGIN = 'https://omnizap.shop';
3
4
  const DEFAULT_BRAND_NAME = 'OmniZap';
4
5
 
@@ -77,7 +78,7 @@ const resolveBrandConfig = (payload = {}) => {
77
78
  const supportFallback = `${siteOrigin}/termos-de-uso/`;
78
79
  const replyToAddress = normalizeEmailAddress(payload?.replyTo || process.env.SMTP_REPLY_TO || process.env.EMAIL_REPLY_TO || process.env.MAIL_REPLY_TO || '');
79
80
  const fromAddress = normalizeEmailAddress(process.env.SMTP_FROM || process.env.EMAIL_FROM || process.env.MAIL_FROM || process.env.SMTP_USER || process.env.EMAIL_USER || process.env.MAIL_USER || '');
80
- const supportPhoneCandidate = normalizePhoneDigits(payload?.supportPhone || process.env.EMAIL_BRAND_SUPPORT_PHONE || process.env.WHATSAPP_SUPPORT_NUMBER || process.env.OWNER_NUMBER || '', 20);
81
+ const supportPhoneCandidate = normalizePhoneDigits(payload?.supportPhone || resolveSupportPhoneFromEnv({ fallback: resolveAdminPhoneFromEnv({ fallback: '' }) }) || '', 20);
81
82
  const supportPhoneDigits = isLikelyPhoneDigits(supportPhoneCandidate) ? supportPhoneCandidate : '';
82
83
  const supportPhonePn = formatPhonePn(supportPhoneDigits);
83
84
  const supportWhatsappUrl = supportPhoneDigits ? `https://wa.me/${supportPhoneDigits}` : '';
@@ -216,7 +217,7 @@ const resolveNavigationLinks = (payload = {}) => {
216
217
  };
217
218
 
218
219
  const resolveWelcomeBotWhatsApp = (payload = {}) => {
219
- const botPhoneCandidate = normalizePhoneDigits(payload?.botPhone || payload?.botNumber || process.env.EMAIL_WELCOME_BOT_PHONE || process.env.WHATSAPP_BOT_NUMBER || process.env.BOT_NUMBER || process.env.BOT_PHONE_NUMBER || process.env.PHONE_NUMBER || process.env.EMAIL_BRAND_SUPPORT_PHONE || '', 20);
220
+ const botPhoneCandidate = normalizePhoneDigits(payload?.botPhone || payload?.botNumber || process.env.EMAIL_WELCOME_BOT_PHONE || resolveBotPhoneFromEnv({ fallback: resolveSupportPhoneFromEnv({ fallback: '' }) }) || '', 20);
220
221
  const botPhoneDigits = isLikelyPhoneDigits(botPhoneCandidate) ? botPhoneCandidate : '';
221
222
  const botPhonePn = formatPhonePn(botPhoneDigits);
222
223
  const botWhatsAppUrl = botPhoneDigits ? `https://wa.me/${botPhoneDigits}` : '';
@@ -6,6 +6,7 @@ import { getMetricsServerConfig, isMetricsEnabled, recordHttpRequest, resolveRou
6
6
  import { applyCachePolicy } from '../middleware/cachePolicy.js';
7
7
  import { applySensitiveRouteRateLimit } from '../middleware/endpointRateLimit.js';
8
8
  import { applySecurityHeaders } from '../middleware/securityHeaders.js';
9
+ import { shouldHandleGrafanaProxyPath } from '../routes/observability/grafanaProxyRouter.js';
9
10
  import { getIndexRouteConfigs, routeRequest } from '../routes/indexRouter.js';
10
11
  import { parseRequestUrl, normalizeRequestId } from './requestContext.js';
11
12
 
@@ -65,6 +66,7 @@ export const startHttpServer = () => {
65
66
  userConfig: routeConfigs?.userConfig || null,
66
67
  systemAdminConfig: routeConfigs?.systemAdminConfig || null,
67
68
  });
69
+ const isGrafanaProxyRequest = shouldHandleGrafanaProxyPath(pathname, routeConfigs?.grafanaProxyConfig || null);
68
70
 
69
71
  res.once('finish', () => {
70
72
  recordHttpRequest({
@@ -76,10 +78,12 @@ export const startHttpServer = () => {
76
78
  });
77
79
 
78
80
  try {
79
- applySecurityHeaders(req, res);
80
- applyCachePolicy(req, res, { pathname });
81
- const allowedByRateLimit = await applySensitiveRouteRateLimit(req, res, { pathname });
82
- if (!allowedByRateLimit) return;
81
+ if (!isGrafanaProxyRequest) {
82
+ applySecurityHeaders(req, res);
83
+ applyCachePolicy(req, res, { pathname });
84
+ const allowedByRateLimit = await applySensitiveRouteRateLimit(req, res, { pathname });
85
+ if (!allowedByRateLimit) return;
86
+ }
83
87
 
84
88
  await routeRequest(req, res, {
85
89
  pathname,
@@ -10,10 +10,29 @@ const parseEnvBool = (value, fallback = false) => {
10
10
  return fallback;
11
11
  };
12
12
 
13
+ const parseEnvList = (value) =>
14
+ String(value || '')
15
+ .split(',')
16
+ .map((item) => String(item || '').trim())
17
+ .filter(Boolean);
18
+
19
+ const toHttpOrigin = (value) => {
20
+ const raw = String(value || '').trim();
21
+ if (!raw) return '';
22
+ try {
23
+ const parsed = new URL(raw);
24
+ if (!['http:', 'https:'].includes(parsed.protocol)) return '';
25
+ return parsed.origin;
26
+ } catch {
27
+ return '';
28
+ }
29
+ };
30
+
13
31
  const HELMET_CSP_ENFORCE = parseEnvBool(process.env.HELMET_CONTENT_SECURITY_POLICY_ENABLED, true);
14
32
  const BACKEND_BUILD_ID = String(process.env.OMNIZAP_BUILD_ID || '')
15
33
  .trim()
16
34
  .slice(0, 80);
35
+ const FRAME_SRC_EXTRA = Array.from(new Set([...parseEnvList(process.env.HELMET_CSP_FRAME_SRC_EXTRA), process.env.SYSTEM_ADMIN_GRAFANA_URL, process.env.GRAFANA_PUBLIC_URL].map((item) => toHttpOrigin(item)).filter(Boolean)));
17
36
 
18
37
  const HELMET_CSP_DIRECTIVES = {
19
38
  defaultSrc: ["'self'"],
@@ -26,7 +45,7 @@ const HELMET_CSP_DIRECTIVES = {
26
45
  imgSrc: ["'self'", 'data:', 'blob:', 'https:'],
27
46
  fontSrc: ["'self'", 'data:', 'https://fonts.gstatic.com', 'https://cdnjs.cloudflare.com'],
28
47
  connectSrc: ["'self'", 'https://accounts.google.com', 'https://oauth2.googleapis.com', 'https://api.github.com'],
29
- frameSrc: ["'self'", 'https://accounts.google.com'],
48
+ frameSrc: ["'self'", 'https://accounts.google.com', ...FRAME_SRC_EXTRA],
30
49
  workerSrc: ["'self'", 'blob:'],
31
50
  manifestSrc: ["'self'"],
32
51
  };
@@ -24,8 +24,10 @@ const DEFAULT_USER_SYSTEM_ADMIN_WEB_PATH = '/user/systemadm';
24
24
  const DEFAULT_LEGACY_STICKER_ADMIN_WEB_PATH = '/stickers/admin';
25
25
  const DEFAULT_SYSTEM_ADMIN_API_BASE_PATH = '/api/admin';
26
26
  const DEFAULT_SYSTEM_ADMIN_API_SESSION_PATH = '/api/admin/session';
27
+ const DEFAULT_SYSTEM_ADMIN_API_MULTI_SESSION_PATH = '/api/admin/multi-session';
27
28
  const DEFAULT_LEGACY_STICKER_ADMIN_API_BASE_PATH = '/api/sticker-packs/admin';
28
29
  const DEFAULT_LEGACY_STICKER_ADMIN_API_SESSION_PATH = '/api/sticker-packs/admin/session';
30
+ const DEFAULT_LEGACY_STICKER_ADMIN_API_MULTI_SESSION_PATH = '/api/sticker-packs/admin/multi-session';
29
31
 
30
32
  export const getSystemAdminRouterConfig = async () => {
31
33
  const controller = await loadSystemAdminController();
@@ -35,8 +37,10 @@ export const getSystemAdminRouterConfig = async () => {
35
37
  legacyWebPath: normalizeBasePath(legacyConfig.legacyWebPath, DEFAULT_LEGACY_STICKER_ADMIN_WEB_PATH),
36
38
  apiAdminBasePath: normalizeBasePath(legacyConfig.apiAdminBasePath, DEFAULT_SYSTEM_ADMIN_API_BASE_PATH),
37
39
  apiAdminSessionPath: normalizeBasePath(legacyConfig.apiAdminSessionPath, DEFAULT_SYSTEM_ADMIN_API_SESSION_PATH),
40
+ apiAdminMultiSessionPath: normalizeBasePath(legacyConfig.apiAdminMultiSessionPath, DEFAULT_SYSTEM_ADMIN_API_MULTI_SESSION_PATH),
38
41
  legacyApiAdminBasePath: normalizeBasePath(legacyConfig.legacyApiAdminBasePath, DEFAULT_LEGACY_STICKER_ADMIN_API_BASE_PATH),
39
42
  legacyApiAdminSessionPath: normalizeBasePath(legacyConfig.legacyApiAdminSessionPath, DEFAULT_LEGACY_STICKER_ADMIN_API_SESSION_PATH),
43
+ legacyApiAdminMultiSessionPath: normalizeBasePath(legacyConfig.legacyApiAdminMultiSessionPath, DEFAULT_LEGACY_STICKER_ADMIN_API_MULTI_SESSION_PATH),
40
44
  };
41
45
  };
42
46
 
@@ -46,8 +50,10 @@ export const shouldHandleSystemAdminPath = (pathname, systemAdminConfig = null)
46
50
  legacyWebPath: DEFAULT_LEGACY_STICKER_ADMIN_WEB_PATH,
47
51
  apiAdminBasePath: DEFAULT_SYSTEM_ADMIN_API_BASE_PATH,
48
52
  apiAdminSessionPath: DEFAULT_SYSTEM_ADMIN_API_SESSION_PATH,
53
+ apiAdminMultiSessionPath: DEFAULT_SYSTEM_ADMIN_API_MULTI_SESSION_PATH,
49
54
  legacyApiAdminBasePath: DEFAULT_LEGACY_STICKER_ADMIN_API_BASE_PATH,
50
55
  legacyApiAdminSessionPath: DEFAULT_LEGACY_STICKER_ADMIN_API_SESSION_PATH,
56
+ legacyApiAdminMultiSessionPath: DEFAULT_LEGACY_STICKER_ADMIN_API_MULTI_SESSION_PATH,
51
57
  };
52
58
 
53
59
  if (startsWithPath(pathname, resolvedConfig.webPath)) return true;
@@ -9,6 +9,7 @@ import { getStickerSiteRouterConfig, maybeHandleStickerSiteRequest, shouldHandle
9
9
  import { getStickerDataRouterConfig, maybeHandleStickerDataRequest, shouldHandleStickerDataPath } from './sticker/stickerDataRouter.js';
10
10
  import { getStickerApiRouterConfig, maybeHandleStickerApiRequest, shouldHandleStickerApiPath } from './sticker/stickerApiRouter.js';
11
11
  import { maybeHandleStaticPageRequest, shouldHandleStaticPagePath } from './static/staticPageRouter.js';
12
+ import { getGrafanaProxyRouterConfig, maybeHandleGrafanaProxyRequest, shouldHandleGrafanaProxyPath } from './observability/grafanaProxyRouter.js';
12
13
 
13
14
  const startsWithPath = (pathname, prefix) => {
14
15
  if (!pathname || !prefix) return false;
@@ -112,12 +113,27 @@ const loadStickerApiConfigSafe = async () => {
112
113
  }
113
114
  };
114
115
 
116
+ const loadGrafanaProxyConfigSafe = async () => {
117
+ try {
118
+ return getGrafanaProxyRouterConfig();
119
+ } catch {
120
+ return {
121
+ enabled: false,
122
+ basePath: '/api/grafana',
123
+ legacyBasePath: '/grafana',
124
+ timeoutMs: 15000,
125
+ target: null,
126
+ };
127
+ }
128
+ };
129
+
115
130
  export const getIndexRouteConfigs = async () => {
116
131
  if (!indexRouteConfigsPromise) {
117
- indexRouteConfigsPromise = Promise.all([loadUserConfigSafe(), loadSystemAdminConfigSafe(), loadEmailAutomationConfigSafe(), loadStickerSiteConfigSafe(), loadStickerDataConfigSafe(), loadStickerApiConfigSafe()]).then(([userConfig, systemAdminConfig, emailAutomationConfig, stickerSiteConfig, stickerDataConfig, stickerApiConfig]) => ({
132
+ indexRouteConfigsPromise = Promise.all([loadUserConfigSafe(), loadSystemAdminConfigSafe(), loadEmailAutomationConfigSafe(), loadStickerSiteConfigSafe(), loadStickerDataConfigSafe(), loadStickerApiConfigSafe(), loadGrafanaProxyConfigSafe()]).then(([userConfig, systemAdminConfig, emailAutomationConfig, stickerSiteConfig, stickerDataConfig, stickerApiConfig, grafanaProxyConfig]) => ({
118
133
  userConfig,
119
134
  systemAdminConfig,
120
135
  emailAutomationConfig,
136
+ grafanaProxyConfig,
121
137
  stickerConfig: {
122
138
  ...stickerSiteConfig,
123
139
  ...stickerDataConfig,
@@ -140,6 +156,7 @@ export const routeRequest = async (req, res, { pathname, url, metricsPath = '/me
140
156
  const userConfig = resolvedConfigs?.userConfig || null;
141
157
  const systemAdminConfig = resolvedConfigs?.systemAdminConfig || null;
142
158
  const emailAutomationConfig = resolvedConfigs?.emailAutomationConfig || null;
159
+ const grafanaProxyConfig = resolvedConfigs?.grafanaProxyConfig || null;
143
160
  const stickerConfig = resolvedConfigs?.stickerConfig || null;
144
161
 
145
162
  // 1) Metrics
@@ -163,7 +180,14 @@ export const routeRequest = async (req, res, { pathname, url, metricsPath = '/me
163
180
  return sendNotFound(req, res);
164
181
  }
165
182
 
166
- // 4) User
183
+ // 4) Grafana proxy (/api/grafana e alias /grafana)
184
+ if (shouldHandleGrafanaProxyPath(pathname, grafanaProxyConfig)) {
185
+ const handled = await maybeHandleGrafanaProxyRequest(req, res, { pathname, url, config: grafanaProxyConfig });
186
+ if (handled) return true;
187
+ return sendNotFound(req, res);
188
+ }
189
+
190
+ // 5) User
167
191
  const systemAdminCandidate = shouldHandleSystemAdminStep(pathname, systemAdminConfig);
168
192
  if (shouldHandleUserStep(pathname, userConfig)) {
169
193
  const handled = await maybeHandleUserRequest(req, res, { pathname, url });
@@ -173,14 +197,14 @@ export const routeRequest = async (req, res, { pathname, url, metricsPath = '/me
173
197
  if (!systemAdminCandidate) return sendNotFound(req, res);
174
198
  }
175
199
 
176
- // 5) System admin + legacy /stickers/admin
200
+ // 6) System admin + legacy /stickers/admin
177
201
  if (systemAdminCandidate) {
178
202
  const handled = await maybeHandleSystemAdminRequest(req, res, { pathname, url });
179
203
  if (handled) return true;
180
204
  return sendNotFound(req, res);
181
205
  }
182
206
 
183
- // 6) Sticker catalog apenas nos prefixes permitidos
207
+ // 7) Sticker catalog apenas nos prefixes permitidos
184
208
  if (shouldHandleStickerSitePath(pathname, stickerConfig)) {
185
209
  const handled = await maybeHandleStickerSiteRequest(req, res, { pathname, url });
186
210
  if (handled) return true;
@@ -212,14 +236,14 @@ export const routeRequest = async (req, res, { pathname, url, metricsPath = '/me
212
236
  return sendNotFound(req, res);
213
237
  }
214
238
 
215
- // 7) Paginas estaticas (templates em public/pages)
239
+ // 8) Paginas estaticas (templates em public/pages)
216
240
  if (shouldHandleStaticPagePath(pathname)) {
217
241
  const handled = await maybeHandleStaticPageRequest(req, res, { pathname });
218
242
  if (handled) return true;
219
243
  return sendNotFound(req, res);
220
244
  }
221
245
 
222
- // 8) 404 global
246
+ // 9) 404 global
223
247
  return sendNotFound(req, res);
224
248
  };
225
249