@omnizap-system/omnizap 2.6.0 → 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 (261) hide show
  1. package/.env.example +58 -13
  2. package/.github/workflows/ci.yml +5 -5
  3. package/.github/workflows/codeql.yml +1 -1
  4. package/.github/workflows/db-migration-check.yml +2 -2
  5. package/.github/workflows/dependency-review.yml +1 -1
  6. package/.github/workflows/deploy.yml +2 -2
  7. package/.github/workflows/release.yml +2 -2
  8. package/.github/workflows/security-attest-provenance.yml +2 -2
  9. package/.github/workflows/security-gitleaks.yml +13 -4
  10. package/.github/workflows/security-runner-hardening.yml +2 -2
  11. package/.github/workflows/security-scorecard.yml +1 -1
  12. package/.github/workflows/security-zap-baseline.yml +1 -1
  13. package/.github/workflows/security-zap-full-scan.yml +2 -1
  14. package/.github/workflows/security-zizmor.yml +1 -1
  15. package/.github/workflows/wiki-sync.yml +1 -1
  16. package/.gitleaksignore +9 -0
  17. package/CODE_OF_CONDUCT.md +2 -2
  18. package/GEMINI.md +64 -0
  19. package/README.md +52 -82
  20. package/SECURITY.md +1 -1
  21. package/app/config/index.js +2 -0
  22. package/app/configParts/adminIdentity.js +5 -5
  23. package/app/configParts/baileysConfig.js +230 -58
  24. package/app/configParts/groupUtils.js +5 -0
  25. package/app/configParts/messagePersistenceService.js +145 -4
  26. package/app/configParts/sessionConfig.js +157 -0
  27. package/app/connection/baileysCompatibility.test.js +1 -1
  28. package/app/connection/groupOwnerWriteStateResolver.js +109 -0
  29. package/app/connection/socketController.js +660 -158
  30. package/app/connection/socketController.multiSession.test.js +108 -0
  31. package/app/controllers/messageController.js +1 -1
  32. package/app/controllers/messagePipeline/commandMiddleware.js +12 -10
  33. package/app/controllers/messagePipeline/conversationMiddleware.js +2 -1
  34. package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +104 -0
  35. package/app/controllers/messagePipeline/preProcessingMiddlewares.js +80 -2
  36. package/app/controllers/messageProcessingPipeline.js +93 -13
  37. package/app/controllers/messageProcessingPipeline.test.js +200 -0
  38. package/app/modules/adminModule/AGENT.md +1 -1
  39. package/app/modules/adminModule/commandConfig.json +3318 -1347
  40. package/app/modules/adminModule/groupCommandHandlers.js +858 -15
  41. package/app/modules/adminModule/groupCommandHandlers.test.js +378 -11
  42. package/app/modules/adminModule/groupWarningRepository.js +152 -0
  43. package/app/modules/aiModule/AGENT.md +47 -30
  44. package/app/modules/aiModule/aiConfigRuntime.js +1 -0
  45. package/app/modules/aiModule/catCommand.js +135 -27
  46. package/app/modules/aiModule/commandConfig.json +114 -28
  47. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +54 -6
  48. package/app/modules/gameModule/AGENT.md +1 -1
  49. package/app/modules/gameModule/commandConfig.json +29 -0
  50. package/app/modules/menuModule/AGENT.md +1 -1
  51. package/app/modules/menuModule/commandConfig.json +45 -10
  52. package/app/modules/menuModule/menuCatalogService.js +190 -0
  53. package/app/modules/menuModule/menuCommandUsageRepository.js +109 -0
  54. package/app/modules/menuModule/menuDynamicService.js +511 -0
  55. package/app/modules/menuModule/menuDynamicService.test.js +141 -0
  56. package/app/modules/menuModule/menus.js +36 -5
  57. package/app/modules/playModule/AGENT.md +10 -5
  58. package/app/modules/playModule/commandConfig.json +140 -12
  59. package/app/modules/playModule/playCommand.js +1 -1417
  60. package/app/modules/playModule/playCommandConstants.js +80 -0
  61. package/app/modules/playModule/playCommandCore.js +361 -0
  62. package/app/modules/playModule/playCommandHandlers.js +41 -0
  63. package/app/modules/playModule/playCommandMediaClient.js +1872 -0
  64. package/app/modules/playModule/playConfigRuntime.js +245 -4
  65. package/app/modules/playModule/playModuleCriticalFlows.test.js +152 -0
  66. package/app/modules/quoteModule/AGENT.md +1 -1
  67. package/app/modules/quoteModule/commandConfig.json +29 -0
  68. package/app/modules/quoteModule/quoteCommand.js +3 -2
  69. package/app/modules/rpgPokemonModule/AGENT.md +1 -1
  70. package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
  71. package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +5 -4
  72. package/app/modules/rpgPokemonModule/rpgBattleService.test.js +2 -1
  73. package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +2 -1
  74. package/app/modules/rpgPokemonModule/rpgPokemonService.js +38 -37
  75. package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +4 -3
  76. package/app/modules/statsModule/AGENT.md +1 -1
  77. package/app/modules/statsModule/commandConfig.json +58 -0
  78. package/app/modules/statsModule/rankingCommon.js +5 -4
  79. package/app/modules/stickerModule/AGENT.md +1 -1
  80. package/app/modules/stickerModule/addStickerMetadata.js +4 -3
  81. package/app/modules/stickerModule/commandConfig.json +145 -0
  82. package/app/modules/stickerModule/stickerCommand.js +1 -1
  83. package/app/modules/stickerPackModule/AGENT.md +1 -1
  84. package/app/modules/stickerPackModule/autoPackCollectorService.js +5 -1
  85. package/app/modules/stickerPackModule/commandConfig.json +29 -0
  86. package/app/modules/stickerPackModule/semanticThemeClusterService.js +7 -6
  87. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +10 -9
  88. package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +9 -8
  89. package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +3 -2
  90. package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +2 -1
  91. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +80 -58
  92. package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +2 -1
  93. package/app/modules/stickerPackModule/stickerPackRepository.js +2 -1
  94. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +5 -4
  95. package/app/modules/stickerPackModule/stickerPackService.js +13 -6
  96. package/app/modules/stickerPackModule/stickerStorageService.js +3 -2
  97. package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +2 -1
  98. package/app/modules/systemMetricsModule/AGENT.md +1 -1
  99. package/app/modules/systemMetricsModule/commandConfig.json +29 -0
  100. package/app/modules/systemMetricsModule/pingCommand.js +6 -5
  101. package/app/modules/tiktokModule/AGENT.md +1 -1
  102. package/app/modules/tiktokModule/commandConfig.json +29 -0
  103. package/app/modules/tiktokModule/tiktokCommand.js +2 -1
  104. package/app/modules/userModule/AGENT.md +1 -1
  105. package/app/modules/userModule/commandConfig.json +29 -0
  106. package/app/modules/userModule/userCommand.js +72 -23
  107. package/app/modules/waifuPicsModule/AGENT.md +57 -27
  108. package/app/modules/waifuPicsModule/commandConfig.json +87 -0
  109. package/app/modules/waifuPicsModule/waifuPicsCommand.js +3 -2
  110. package/app/observability/metrics.js +136 -0
  111. package/app/services/ai/commandConfigEnrichmentService.js +229 -47
  112. package/app/services/ai/conversationRouterService.js +4 -3
  113. package/app/services/ai/geminiService.js +132 -7
  114. package/app/services/ai/geminiService.test.js +59 -2
  115. package/app/services/ai/globalModuleAiHelpService.js +3 -2
  116. package/app/services/ai/messageCommandExecutionService.js +2 -1
  117. package/app/services/ai/moduleAiHelpCoreService.js +45 -14
  118. package/app/services/ai/moduleToolExecutorService.js +3 -2
  119. package/app/services/ai/moduleToolRegistryService.js +2 -1
  120. package/app/services/ai/toolCandidateSelectorService.js +6 -5
  121. package/app/services/auth/googleWebLinkService.js +3 -2
  122. package/app/services/auth/whatsappLoginLinkService.js +3 -2
  123. package/app/services/external/pokeApiService.js +4 -3
  124. package/app/services/group/groupMetadataService.js +24 -1
  125. package/app/services/infra/dbWriteQueue.js +57 -26
  126. package/app/services/infra/featureFlagService.js +2 -1
  127. package/app/services/messaging/captchaService.js +3 -2
  128. package/app/services/messaging/newsBroadcastService.js +846 -29
  129. package/app/services/multiSession/assignmentBalancerService.js +457 -0
  130. package/app/services/multiSession/groupOwnershipRepository.js +381 -0
  131. package/app/services/multiSession/groupOwnershipService.js +890 -0
  132. package/app/services/multiSession/groupOwnershipService.test.js +309 -0
  133. package/app/services/multiSession/sessionRegistryService.js +293 -0
  134. package/app/services/sticker/stickerFocusService.js +11 -10
  135. package/app/store/aiPromptStore.js +36 -19
  136. package/app/store/conversationSessionStore.js +7 -6
  137. package/app/store/groupConfigStore.js +41 -5
  138. package/app/store/premiumUserStore.js +21 -7
  139. package/app/utils/antiLink/antiLinkModule.js +352 -16
  140. package/app/workers/aiHelperContinuousLearningWorker.js +512 -0
  141. package/app/workers/aiLearningWorker.js +6 -5
  142. package/app/workers/commandConfigEnrichmentWorker.js +4 -3
  143. package/database/index.js +14 -8
  144. package/database/migrations/20260307_d0_hardening_down.sql +1 -1
  145. package/database/migrations/20260314_d7_canonical_sender_down.sql +1 -1
  146. package/database/migrations/20260406_d30_security_analytics_down.sql +1 -1
  147. package/database/migrations/20260411_d35_group_community_metadata_down.sql +59 -0
  148. package/database/migrations/20260411_d35_group_community_metadata_up.sql +62 -0
  149. package/database/migrations/20260412_d36_system_config_tables_down.sql +32 -0
  150. package/database/migrations/20260412_d36_system_config_tables_up.sql +66 -0
  151. package/database/migrations/20260413_d37_group_user_warnings_down.sql +11 -0
  152. package/database/migrations/20260413_d37_group_user_warnings_up.sql +24 -0
  153. package/database/migrations/20260414_d38_multi_session_foundation_down.sql +72 -0
  154. package/database/migrations/20260414_d38_multi_session_foundation_up.sql +125 -0
  155. package/database/migrations/20260414_d39_multi_session_cutover_down.sql +103 -0
  156. package/database/migrations/20260414_d39_multi_session_cutover_up.sql +83 -0
  157. package/database/schema.sql +102 -1
  158. package/docker-compose.yml +4 -1
  159. package/docs/compliance/acceptable-use-policy-2026-03-07.md +1 -1
  160. package/docs/compliance/dpa-b2b-standard-2026-03-07.md +1 -1
  161. package/docs/compliance/privacy-policy-2026-03-07.md +4 -4
  162. package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
  163. package/docs/security/incident-response-lgpd-anpd-runbook-2026-03-07.md +1 -1
  164. package/docs/security/network-hardening-runbook-2026-03-07.md +53 -0
  165. package/docs/security/omnizap-static-security-headers.conf +25 -0
  166. package/docs/wiki/Home.md +1 -1
  167. package/ecosystem.prod.config.cjs +32 -12
  168. package/index.js +57 -23
  169. package/observability/alert-rules.yml +20 -0
  170. package/observability/grafana/dashboards/omnizap-system-admin.json +229 -0
  171. package/observability/mysql-setup.sql +4 -4
  172. package/observability/system-admin-observability.md +26 -0
  173. package/package.json +20 -6
  174. package/public/apple-touch-icon.png +0 -0
  175. package/public/comandos/commands-catalog.json +2853 -3326
  176. package/public/favicon-16x16.png +0 -0
  177. package/public/favicon-32x32.png +0 -0
  178. package/public/favicon.ico +0 -0
  179. package/public/js/apps/apiDocsApp.js +3 -2
  180. package/public/js/apps/commandsReactApp.js +280 -99
  181. package/public/js/apps/createPackApp.js +11 -10
  182. package/public/js/apps/homeReactApp.js +181 -130
  183. package/public/js/apps/loginReactApp.js +1 -1
  184. package/public/js/apps/stickersApp.js +263 -110
  185. package/public/js/apps/termsReactApp.js +73 -24
  186. package/public/js/apps/userApp.js +4 -3
  187. package/public/js/apps/userPasswordResetReactApp.js +406 -0
  188. package/public/js/apps/userReactApp.js +355 -280
  189. package/public/js/apps/userSystemAdmReactApp.js +1506 -0
  190. package/public/pages/api-docs.html +1 -1
  191. package/public/pages/aup.html +2 -2
  192. package/public/pages/dpa.html +3 -3
  193. package/public/pages/licenca.html +4 -4
  194. package/public/pages/login.html +1 -1
  195. package/public/pages/notice-and-takedown.html +2 -2
  196. package/public/pages/politica-de-privacidade.html +6 -6
  197. package/public/pages/seo-bot-whatsapp-para-grupo.html +3 -3
  198. package/public/pages/seo-bot-whatsapp-sem-programar.html +3 -3
  199. package/public/pages/seo-como-automatizar-avisos-no-whatsapp.html +3 -3
  200. package/public/pages/seo-como-criar-comandos-whatsapp.html +3 -3
  201. package/public/pages/seo-como-evitar-spam-no-whatsapp.html +3 -3
  202. package/public/pages/seo-como-moderar-grupo-whatsapp.html +3 -3
  203. package/public/pages/seo-como-organizar-comunidade-whatsapp.html +3 -3
  204. package/public/pages/seo-melhor-bot-whatsapp-para-grupos.html +3 -3
  205. package/public/pages/stickers-admin.html +1 -1
  206. package/public/pages/stickers-create.html +1 -1
  207. package/public/pages/stickers.html +6 -6
  208. package/public/pages/suboperadores.html +2 -2
  209. package/public/pages/termos-de-uso-texto-integral.html +6 -6
  210. package/public/pages/termos-de-uso.html +3 -3
  211. package/public/pages/user-password-reset.html +4 -5
  212. package/public/pages/user-systemadm.html +9 -463
  213. package/public/pages/user.html +2 -2
  214. package/scripts/clear-whatsapp-session.sh +123 -0
  215. package/scripts/core-ai-mode.mjs +163 -0
  216. package/scripts/deploy.sh +11 -1
  217. package/scripts/email-broadcast-terms-update.mjs +2 -1
  218. package/scripts/enrich-command-config-ux-openai.mjs +492 -0
  219. package/scripts/generate-commands-catalog.mjs +166 -2
  220. package/scripts/generate-module-agents.mjs +2 -1
  221. package/scripts/generate-seo-satellite-pages.mjs +5 -4
  222. package/scripts/github-deploy-notify.mjs +2 -1
  223. package/scripts/github-release-notify.mjs +25 -10
  224. package/scripts/new-whatsapp-session.sh +317 -0
  225. package/scripts/release.sh +2 -19
  226. package/scripts/security-smoketest.mjs +6 -5
  227. package/scripts/security-web-surface-check.mjs +218 -0
  228. package/scripts/sticker-catalog-loadtest.mjs +5 -4
  229. package/server/auth/googleWebAuth/googleWebAuthService.js +8 -7
  230. package/server/auth/jwt/webJwtService.js +1 -1
  231. package/server/auth/stickerCatalogAuthContext.js +2 -1
  232. package/server/auth/termsAcceptance/termsAcceptanceHandler.js +2 -1
  233. package/server/auth/userPassword/userPasswordAuthService.js +2 -1
  234. package/server/auth/userPassword/userPasswordRecoveryService.js +4 -3
  235. package/server/auth/webAccount/webAccountHandlers.js +9 -10
  236. package/server/controllers/admin/adminPanelHandlers.js +267 -16
  237. package/server/controllers/admin/systemAdminController.js +267 -0
  238. package/server/controllers/seo/stickerCatalogSeoContext.js +10 -9
  239. package/server/controllers/sticker/nonCatalogHandlers.js +2 -1
  240. package/server/controllers/sticker/stickerCatalogController.js +23 -36
  241. package/server/controllers/system/contactController.js +9 -17
  242. package/server/controllers/system/githubController.js +3 -2
  243. package/server/controllers/system/stickerCatalogSystemContext.js +41 -19
  244. package/server/controllers/system/systemController.js +254 -1
  245. package/server/controllers/system/systemMetricsController.js +2 -1
  246. package/server/controllers/userController.js +6 -0
  247. package/server/email/emailTemplateService.js +5 -3
  248. package/server/http/httpServer.js +11 -6
  249. package/server/middleware/rateLimit.js +2 -1
  250. package/server/middleware/securityHeaders.js +20 -1
  251. package/server/routes/admin/systemAdminRouter.js +6 -0
  252. package/server/routes/indexRouter.js +30 -6
  253. package/server/routes/observability/grafanaProxyRouter.js +254 -0
  254. package/server/routes/static/staticPageRouter.js +27 -1
  255. package/server/utils/publicContact.js +31 -0
  256. package/utils/time/timeModule.js +135 -0
  257. package/utils/time/timeModule.test.js +65 -0
  258. package/utils/whatsapp/contactEnv.js +39 -0
  259. package/vite.config.mjs +7 -1
  260. package/public/assets/images/brand-icon-192.png +0 -0
  261. package/scripts/sync-readme-snapshot.mjs +0 -133
@@ -1,4 +1,6 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
1
2
  import { withTimeout } from '../../http/httpRequestUtils.js';
3
+ import { resolveBotPhoneFromEnv } from '../../../utils/whatsapp/contactEnv.js';
2
4
 
3
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 }) => {
4
6
  let globalRankRefreshTimer = null;
@@ -125,7 +127,7 @@ export const createStickerCatalogSystemContext = ({ executeQuery, tables, logger
125
127
  http_latency_p95_ms: prometheus?.http_latency_p95_ms ?? null,
126
128
  queue_peak: prometheus?.queue_peak ?? null,
127
129
  },
128
- updated_at: new Date().toISOString(),
130
+ updated_at: __timeNowIso(),
129
131
  },
130
132
  meta: {
131
133
  metrics_endpoint: metricsEndpoint,
@@ -138,7 +140,7 @@ export const createStickerCatalogSystemContext = ({ executeQuery, tables, logger
138
140
  };
139
141
 
140
142
  const getSystemSummaryCached = async () => {
141
- const now = Date.now();
143
+ const now = __timeNowMs();
142
144
  const hasValue = Boolean(systemSummaryCache.value);
143
145
 
144
146
  if (hasValue && now < systemSummaryCache.expiresAt) {
@@ -149,9 +151,15 @@ export const createStickerCatalogSystemContext = ({ executeQuery, tables, logger
149
151
  systemSummaryCache.pending = withTimeout(buildSystemSummarySnapshot(), 5000)
150
152
  .then((payload) => {
151
153
  systemSummaryCache.value = payload;
152
- systemSummaryCache.expiresAt = Date.now() + systemSummaryCacheSeconds * 1000;
154
+ systemSummaryCache.expiresAt = __timeNowMs() + systemSummaryCacheSeconds * 1000;
153
155
  return payload;
154
156
  })
157
+ .catch((error) => {
158
+ if (hasValue && systemSummaryCache.value) {
159
+ return systemSummaryCache.value;
160
+ }
161
+ throw error;
162
+ })
155
163
  .finally(() => {
156
164
  systemSummaryCache.pending = null;
157
165
  });
@@ -278,7 +286,7 @@ export const createStickerCatalogSystemContext = ({ executeQuery, tables, logger
278
286
  .slice(0, 8);
279
287
 
280
288
  const commands = collectAvailableMenuCommands(readmeCommandPrefix);
281
- const generatedAt = new Date().toISOString();
289
+ const generatedAt = __timeNowIso();
282
290
 
283
291
  const totals = {
284
292
  total_users: Number(lidMapTotals?.total_users || 0),
@@ -314,7 +322,7 @@ export const createStickerCatalogSystemContext = ({ executeQuery, tables, logger
314
322
  };
315
323
 
316
324
  const getReadmeSummaryCached = async () => {
317
- const now = Date.now();
325
+ const now = __timeNowMs();
318
326
  const hasValue = Boolean(readmeSummaryCache.value);
319
327
 
320
328
  if (hasValue && now < readmeSummaryCache.expiresAt) {
@@ -325,9 +333,15 @@ export const createStickerCatalogSystemContext = ({ executeQuery, tables, logger
325
333
  readmeSummaryCache.pending = withTimeout(buildReadmeSummarySnapshot(), 7000)
326
334
  .then((payload) => {
327
335
  readmeSummaryCache.value = payload;
328
- readmeSummaryCache.expiresAt = Date.now() + readmeSummaryCacheSeconds * 1000;
336
+ readmeSummaryCache.expiresAt = __timeNowMs() + readmeSummaryCacheSeconds * 1000;
329
337
  return payload;
330
338
  })
339
+ .catch((error) => {
340
+ if (hasValue && readmeSummaryCache.value) {
341
+ return readmeSummaryCache.value;
342
+ }
343
+ throw error;
344
+ })
331
345
  .finally(() => {
332
346
  readmeSummaryCache.pending = null;
333
347
  });
@@ -345,12 +359,8 @@ export const createStickerCatalogSystemContext = ({ executeQuery, tables, logger
345
359
  const botPhoneFromCatalog = String(resolveCatalogBotPhone() || '').replace(/\D+/g, '');
346
360
  if (botPhoneFromCatalog) candidates.add(botPhoneFromCatalog);
347
361
 
348
- const envCandidates = [process.env.WHATSAPP_BOT_NUMBER, process.env.BOT_NUMBER, process.env.PHONE_NUMBER, process.env.BOT_PHONE_NUMBER];
349
-
350
- for (const candidate of envCandidates) {
351
- const digits = String(candidate || '').replace(/\D+/g, '');
352
- if (digits) candidates.add(digits);
353
- }
362
+ const configuredBotPhone = String(resolveBotPhoneFromEnv({ fallback: '' }) || '').replace(/\D+/g, '');
363
+ if (configuredBotPhone) candidates.add(configuredBotPhone);
354
364
 
355
365
  return Array.from(candidates).filter((value) => value.length >= 8);
356
366
  };
@@ -522,12 +532,12 @@ export const createStickerCatalogSystemContext = ({ executeQuery, tables, logger
522
532
  top_type: topType,
523
533
  top_type_count: topTypeCount,
524
534
  rows: rowsEnriched,
525
- updated_at: new Date().toISOString(),
535
+ updated_at: __timeNowIso(),
526
536
  };
527
537
  };
528
538
 
529
539
  const getGlobalRankingSummaryCached = async () => {
530
- const now = Date.now();
540
+ const now = __timeNowMs();
531
541
  const hasValue = Boolean(globalRankCache.value);
532
542
 
533
543
  if (hasValue && now < globalRankCache.expiresAt) {
@@ -538,9 +548,15 @@ export const createStickerCatalogSystemContext = ({ executeQuery, tables, logger
538
548
  globalRankCache.pending = withTimeout(buildGlobalRankingSummary(), 5000)
539
549
  .then((data) => {
540
550
  globalRankCache.value = data;
541
- globalRankCache.expiresAt = Date.now() + globalRankRefreshSeconds * 1000;
551
+ globalRankCache.expiresAt = __timeNowMs() + globalRankRefreshSeconds * 1000;
542
552
  return data;
543
553
  })
554
+ .catch((error) => {
555
+ if (hasValue && globalRankCache.value) {
556
+ return globalRankCache.value;
557
+ }
558
+ throw error;
559
+ })
544
560
  .finally(() => {
545
561
  globalRankCache.pending = null;
546
562
  });
@@ -579,7 +595,7 @@ export const createStickerCatalogSystemContext = ({ executeQuery, tables, logger
579
595
  };
580
596
 
581
597
  const buildLastSevenUtcDateKeys = () => {
582
- const now = new Date();
598
+ const now = __timeNow();
583
599
  const todayUtc = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
584
600
  return Array.from({ length: 7 }).map((_, index) => {
585
601
  const date = new Date(todayUtc - (6 - index) * 24 * 60 * 60 * 1000);
@@ -718,12 +734,12 @@ export const createStickerCatalogSystemContext = ({ executeQuery, tables, logger
718
734
  clicks_last_7_days: Number(clicksLast7Days || 0),
719
735
  likes_last_7_days: Number(likesLast7Days || 0),
720
736
  series_last_7_days: seriesLast7Days,
721
- updated_at: new Date().toISOString(),
737
+ updated_at: __timeNowIso(),
722
738
  };
723
739
  };
724
740
 
725
741
  const getMarketplaceGlobalStatsCached = async () => {
726
- const now = Date.now();
742
+ const now = __timeNowMs();
727
743
  const hasValue = Boolean(marketplaceGlobalStatsCache.value);
728
744
  if (hasValue && now < marketplaceGlobalStatsCache.expiresAt) {
729
745
  return marketplaceGlobalStatsCache.value;
@@ -733,9 +749,15 @@ export const createStickerCatalogSystemContext = ({ executeQuery, tables, logger
733
749
  marketplaceGlobalStatsCache.pending = withTimeout(buildMarketplaceGlobalStatsSnapshot(), 5000)
734
750
  .then((data) => {
735
751
  marketplaceGlobalStatsCache.value = data;
736
- marketplaceGlobalStatsCache.expiresAt = Date.now() + marketplaceGlobalStatsCacheSeconds * 1000;
752
+ marketplaceGlobalStatsCache.expiresAt = __timeNowMs() + marketplaceGlobalStatsCacheSeconds * 1000;
737
753
  return data;
738
754
  })
755
+ .catch((error) => {
756
+ if (hasValue && marketplaceGlobalStatsCache.value) {
757
+ return marketplaceGlobalStatsCache.value;
758
+ }
759
+ throw error;
760
+ })
739
761
  .finally(() => {
740
762
  marketplaceGlobalStatsCache.pending = null;
741
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 };
@@ -1,3 +1,4 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
1
2
  import { formatDuration } from '../../http/httpRequestUtils.js';
2
3
 
3
4
  const METRICS_ENDPOINT = process.env.METRICS_ENDPOINT || `http://127.0.0.1:${process.env.METRICS_PORT || 9102}${process.env.METRICS_PATH || '/metrics'}`;
@@ -124,7 +125,7 @@ export const fetchPrometheusSummary = async () => {
124
125
  const series = parsePrometheusText(text);
125
126
 
126
127
  const processStart = pickMetricValue(series, 'omnizap_process_start_time_seconds');
127
- const nowSeconds = Date.now() / 1000;
128
+ const nowSeconds = __timeNowMs() / 1000;
128
129
  const processUptimeSeconds = Number.isFinite(processStart) ? Math.max(0, nowSeconds - processStart) : null;
129
130
 
130
131
  const lagP99 = pickMetricValue(series, 'omnizap_nodejs_eventloop_lag_p99_seconds');
@@ -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,3 +1,5 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
2
+ import { resolveAdminPhoneFromEnv, resolveBotPhoneFromEnv, resolveSupportPhoneFromEnv } from '../../utils/whatsapp/contactEnv.js';
1
3
  const DEFAULT_SITE_ORIGIN = 'https://omnizap.shop';
2
4
  const DEFAULT_BRAND_NAME = 'OmniZap';
3
5
 
@@ -76,7 +78,7 @@ const resolveBrandConfig = (payload = {}) => {
76
78
  const supportFallback = `${siteOrigin}/termos-de-uso/`;
77
79
  const replyToAddress = normalizeEmailAddress(payload?.replyTo || process.env.SMTP_REPLY_TO || process.env.EMAIL_REPLY_TO || process.env.MAIL_REPLY_TO || '');
78
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 || '');
79
- 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);
80
82
  const supportPhoneDigits = isLikelyPhoneDigits(supportPhoneCandidate) ? supportPhoneCandidate : '';
81
83
  const supportPhonePn = formatPhonePn(supportPhoneDigits);
82
84
  const supportWhatsappUrl = supportPhoneDigits ? `https://wa.me/${supportPhoneDigits}` : '';
@@ -125,7 +127,7 @@ const renderEmailLayout = ({ payload = {}, preheader = '', heading = '', greetin
125
127
  const safeSecondaryCtaUrl = normalizeHttpUrl(secondaryCtaUrl, '');
126
128
  const safeSecurityNote = normalizeText(securityNote, 220);
127
129
  const safeFooterMessage = normalizeText(footerMessage, 220);
128
- const year = new Date().getUTCFullYear();
130
+ const year = __timeNow().getUTCFullYear();
129
131
 
130
132
  const logoBlock = brand.brandLogoUrl ? `<img src="${escapeHtml(brand.brandLogoUrl)}" alt="${escapeHtml(brand.brandName)}" width="132" style="display:block;border:0;outline:none;text-decoration:none;height:auto;margin:0 auto;" />` : `<div style="display:inline-block;font-size:26px;font-weight:800;color:#0f172a;letter-spacing:0.2px;">${escapeHtml(brand.brandName)}</div>`;
131
133
 
@@ -215,7 +217,7 @@ const resolveNavigationLinks = (payload = {}) => {
215
217
  };
216
218
 
217
219
  const resolveWelcomeBotWhatsApp = (payload = {}) => {
218
- 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);
219
221
  const botPhoneDigits = isLikelyPhoneDigits(botPhoneCandidate) ? botPhoneCandidate : '';
220
222
  const botPhonePn = formatPhonePn(botPhoneDigits);
221
223
  const botWhatsAppUrl = botPhoneDigits ? `https://wa.me/${botPhoneDigits}` : '';
@@ -1,3 +1,4 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
1
2
  import http from 'node:http';
2
3
 
3
4
  import logger from '#logger';
@@ -5,6 +6,7 @@ import { getMetricsServerConfig, isMetricsEnabled, recordHttpRequest, resolveRou
5
6
  import { applyCachePolicy } from '../middleware/cachePolicy.js';
6
7
  import { applySensitiveRouteRateLimit } from '../middleware/endpointRateLimit.js';
7
8
  import { applySecurityHeaders } from '../middleware/securityHeaders.js';
9
+ import { shouldHandleGrafanaProxyPath } from '../routes/observability/grafanaProxyRouter.js';
8
10
  import { getIndexRouteConfigs, routeRequest } from '../routes/indexRouter.js';
9
11
  import { parseRequestUrl, normalizeRequestId } from './requestContext.js';
10
12
 
@@ -41,7 +43,7 @@ export const startHttpServer = () => {
41
43
  const { host, port, path: metricsPath } = getMetricsServerConfig();
42
44
 
43
45
  server = http.createServer(async (req, res) => {
44
- const requestStartedAt = Date.now();
46
+ const requestStartedAt = __timeNowMs();
45
47
  const requestId = normalizeRequestId(req.headers['x-request-id']);
46
48
  res.setHeader('X-Request-Id', requestId);
47
49
 
@@ -64,10 +66,11 @@ export const startHttpServer = () => {
64
66
  userConfig: routeConfigs?.userConfig || null,
65
67
  systemAdminConfig: routeConfigs?.systemAdminConfig || null,
66
68
  });
69
+ const isGrafanaProxyRequest = shouldHandleGrafanaProxyPath(pathname, routeConfigs?.grafanaProxyConfig || null);
67
70
 
68
71
  res.once('finish', () => {
69
72
  recordHttpRequest({
70
- durationMs: Date.now() - requestStartedAt,
73
+ durationMs: __timeNowMs() - requestStartedAt,
71
74
  method: req.method,
72
75
  statusCode: res.statusCode,
73
76
  routeGroup,
@@ -75,10 +78,12 @@ export const startHttpServer = () => {
75
78
  });
76
79
 
77
80
  try {
78
- applySecurityHeaders(req, res);
79
- applyCachePolicy(req, res, { pathname });
80
- const allowedByRateLimit = await applySensitiveRouteRateLimit(req, res, { pathname });
81
- 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
+ }
82
87
 
83
88
  await routeRequest(req, res, {
84
89
  pathname,
@@ -1,3 +1,4 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
1
2
  import { resolveClientIp } from '../http/clientIp.js';
2
3
 
3
4
  const rateLimitBuckets = new Map();
@@ -38,7 +39,7 @@ export const createRateLimit = ({ windowMs = 60_000, max = 60, keyPrefix = 'glob
38
39
  const safeKeyPrefix = String(keyPrefix || 'global').trim() || 'global';
39
40
 
40
41
  return (req, res) => {
41
- const nowMs = Date.now();
42
+ const nowMs = __timeNowMs();
42
43
  pruneBuckets(safeWindowMs, nowMs);
43
44
 
44
45
  const ip = resolveClientIp(req);
@@ -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;