@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,8 +1,17 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import { timingSafeEqual } from 'node:crypto';
3
4
  import { URL } from 'node:url';
4
5
 
5
6
  import logger from '#logger';
7
+ import {
8
+ forceSystemAdminGroupFailover,
9
+ listSystemAdminAssignmentHistory,
10
+ listSystemAdminAssignments,
11
+ listSystemAdminSessions,
12
+ setSystemAdminGroupPin,
13
+ triggerSystemAdminManualRebalance,
14
+ } from '../system/systemController.js';
6
15
 
7
16
  const parseEnvBool = (value, fallback) => {
8
17
  if (value === undefined || value === null || value === '') return fallback;
@@ -25,6 +34,8 @@ const SYSTEM_ADMIN_API_BASE_PATH = normalizeBasePath(process.env.SYSTEM_ADMIN_AP
25
34
  const SYSTEM_ADMIN_API_SESSION_PATH = `${SYSTEM_ADMIN_API_BASE_PATH}/session`;
26
35
  const LEGACY_SYSTEM_ADMIN_API_BASE_PATH = `${LEGACY_STICKER_API_BASE_PATH}/admin`;
27
36
  const LEGACY_SYSTEM_ADMIN_API_SESSION_PATH = `${LEGACY_SYSTEM_ADMIN_API_BASE_PATH}/session`;
37
+ const SYSTEM_ADMIN_MULTI_SESSION_API_PATH = `${SYSTEM_ADMIN_API_BASE_PATH}/multi-session`;
38
+ const LEGACY_SYSTEM_ADMIN_MULTI_SESSION_API_PATH = `${LEGACY_SYSTEM_ADMIN_API_BASE_PATH}/multi-session`;
28
39
  const STICKER_LOGIN_WEB_PATH = normalizeBasePath(process.env.STICKER_LOGIN_WEB_PATH, '/login');
29
40
  const STICKER_WEB_PATH = normalizeBasePath(process.env.STICKER_WEB_PATH, '/stickers');
30
41
  const STICKER_ADMIN_WEB_PATH = `${STICKER_WEB_PATH}/admin`;
@@ -37,6 +48,9 @@ const SITE_ORIGIN = String(process.env.SITE_ORIGIN || 'https://omnizap.shop')
37
48
 
38
49
  const USER_SYSTEMADM_TEMPLATE_PATH = path.join(process.cwd(), 'public', 'pages', 'user-systemadm.html');
39
50
  const LEGACY_STICKER_ADMIN_TEMPLATE_PATH = path.join(process.cwd(), 'public', 'pages', 'stickers-admin.html');
51
+ const SYSTEM_ADMIN_OPS_TOKEN = String(
52
+ process.env.SYSTEM_ADMIN_OPS_TOKEN || process.env.USER_INTERNAL_API_TOKEN || process.env.ADMIN_TOKEN || process.env.ADMIN_API_TOKEN || '',
53
+ ).trim();
40
54
 
41
55
  let stickerCatalogControllerPromise = null;
42
56
  const loadStickerCatalogController = async () => {
@@ -108,6 +122,106 @@ const mapAdminApiPathToLegacy = (pathname) => {
108
122
  return null;
109
123
  };
110
124
 
125
+ const mapMultiSessionApiPath = (pathname) => {
126
+ if (hasPathPrefix(pathname, SYSTEM_ADMIN_MULTI_SESSION_API_PATH)) {
127
+ const suffix = pathname.slice(SYSTEM_ADMIN_MULTI_SESSION_API_PATH.length);
128
+ return suffix || '/';
129
+ }
130
+ if (hasPathPrefix(pathname, LEGACY_SYSTEM_ADMIN_MULTI_SESSION_API_PATH)) {
131
+ const suffix = pathname.slice(LEGACY_SYSTEM_ADMIN_MULTI_SESSION_API_PATH.length);
132
+ return suffix || '/';
133
+ }
134
+ return null;
135
+ };
136
+
137
+ const constantTimeStringEqual = (left, right) => {
138
+ const leftBuffer = Buffer.from(String(left || ''), 'utf8');
139
+ const rightBuffer = Buffer.from(String(right || ''), 'utf8');
140
+ if (!leftBuffer.length || leftBuffer.length !== rightBuffer.length) return false;
141
+ try {
142
+ return timingSafeEqual(leftBuffer, rightBuffer);
143
+ } catch {
144
+ return false;
145
+ }
146
+ };
147
+
148
+ const extractBearerToken = (req) => {
149
+ const authHeader = String(req?.headers?.authorization || '').trim();
150
+ if (!authHeader.toLowerCase().startsWith('bearer ')) return '';
151
+ return authHeader.slice(7).trim();
152
+ };
153
+
154
+ const resolveOpsTokenFromRequest = (req) =>
155
+ String(req?.headers?.['x-system-admin-token'] || req?.headers?.['x-internal-api-token'] || req?.headers?.['x-admin-token'] || '').trim() ||
156
+ extractBearerToken(req);
157
+
158
+ const hasValidOpsToken = (req) => {
159
+ if (!SYSTEM_ADMIN_OPS_TOKEN) return true;
160
+ const requestToken = resolveOpsTokenFromRequest(req);
161
+ if (!requestToken) return false;
162
+ return constantTimeStringEqual(requestToken, SYSTEM_ADMIN_OPS_TOKEN);
163
+ };
164
+
165
+ const readJsonBody = async (req, { maxBytes = 64 * 1024 } = {}) =>
166
+ new Promise((resolve, reject) => {
167
+ const chunks = [];
168
+ let total = 0;
169
+
170
+ req.on('data', (chunk) => {
171
+ total += chunk.length;
172
+ if (total > maxBytes) {
173
+ const error = new Error('Payload excedeu limite permitido.');
174
+ error.statusCode = 413;
175
+ reject(error);
176
+ req.destroy();
177
+ return;
178
+ }
179
+ chunks.push(chunk);
180
+ });
181
+
182
+ req.on('end', () => {
183
+ const raw = Buffer.concat(chunks).toString('utf8').trim();
184
+ if (!raw) {
185
+ resolve({});
186
+ return;
187
+ }
188
+
189
+ try {
190
+ resolve(JSON.parse(raw));
191
+ } catch {
192
+ const error = new Error('JSON invalido.');
193
+ error.statusCode = 400;
194
+ reject(error);
195
+ }
196
+ });
197
+
198
+ req.on('error', (error) => reject(error));
199
+ });
200
+
201
+ const parseBool = (value, fallback = false) => {
202
+ if (value === undefined || value === null || value === '') return fallback;
203
+ const normalized = String(value).trim().toLowerCase();
204
+ if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
205
+ if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
206
+ return fallback;
207
+ };
208
+
209
+ const parsePositiveInt = (value, fallback = 200, min = 1, max = 5000) => {
210
+ const parsed = Number.parseInt(String(value ?? ''), 10);
211
+ if (!Number.isFinite(parsed)) return fallback;
212
+ return Math.max(min, Math.min(max, parsed));
213
+ };
214
+
215
+ const decodePathSegment = (value) => {
216
+ const raw = String(value || '').trim();
217
+ if (!raw) return '';
218
+ try {
219
+ return decodeURIComponent(raw);
220
+ } catch {
221
+ return raw;
222
+ }
223
+ };
224
+
111
225
  const renderUserSystemAdminHtml = async () => {
112
226
  const template = await fs.readFile(USER_SYSTEMADM_TEMPLATE_PATH, 'utf8');
113
227
  const dataAttributes = {
@@ -124,13 +238,147 @@ const renderUserSystemAdminHtml = async () => {
124
238
  return html;
125
239
  };
126
240
 
241
+ const requireSystemAdminOpsAccess = (req, res) => {
242
+ if (hasValidOpsToken(req)) return true;
243
+ sendJson(req, res, 401, { error: 'Nao autorizado para operacoes de system admin.' });
244
+ return false;
245
+ };
246
+
247
+ const normalizeMultiSessionSubPath = (value) => {
248
+ const raw = String(value || '/').trim();
249
+ if (!raw || raw === '/') return '/';
250
+ return `/${raw
251
+ .replace(/^\/+/g, '')
252
+ .replace(/\/+$/g, '')}`;
253
+ };
254
+
255
+ const handleMultiSessionOpsRequest = async (req, res, { pathname, url }) => {
256
+ if (!requireSystemAdminOpsAccess(req, res)) return true;
257
+
258
+ const subPath = normalizeMultiSessionSubPath(pathname);
259
+ const requestUrl = (() => {
260
+ try {
261
+ return new URL(String(url?.href || req.url || '/'), SITE_ORIGIN);
262
+ } catch {
263
+ return new URL(SITE_ORIGIN);
264
+ }
265
+ })();
266
+
267
+ if (subPath === '/sessions') {
268
+ if (!['GET', 'HEAD'].includes(req.method || '')) {
269
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
270
+ return true;
271
+ }
272
+ const payload = await listSystemAdminSessions({
273
+ status: requestUrl.searchParams.get('status'),
274
+ limit: parsePositiveInt(requestUrl.searchParams.get('limit'), 200, 1, 5000),
275
+ });
276
+ sendJson(req, res, 200, payload);
277
+ return true;
278
+ }
279
+
280
+ if (subPath === '/assignments') {
281
+ if (!['GET', 'HEAD'].includes(req.method || '')) {
282
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
283
+ return true;
284
+ }
285
+ const payload = await listSystemAdminAssignments({
286
+ groupJid: requestUrl.searchParams.get('group_jid'),
287
+ ownerSessionId: requestUrl.searchParams.get('owner_session_id'),
288
+ includeExpired: parseBool(requestUrl.searchParams.get('include_expired'), false),
289
+ limit: parsePositiveInt(requestUrl.searchParams.get('limit'), 200, 1, 5000),
290
+ });
291
+ sendJson(req, res, 200, payload);
292
+ return true;
293
+ }
294
+
295
+ if (subPath === '/history') {
296
+ if (!['GET', 'HEAD'].includes(req.method || '')) {
297
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
298
+ return true;
299
+ }
300
+ const payload = await listSystemAdminAssignmentHistory({
301
+ groupJid: requestUrl.searchParams.get('group_jid'),
302
+ limit: parsePositiveInt(requestUrl.searchParams.get('limit'), 100, 1, 5000),
303
+ });
304
+ sendJson(req, res, 200, payload);
305
+ return true;
306
+ }
307
+
308
+ if (subPath === '/rebalance') {
309
+ if (!['POST'].includes(req.method || '')) {
310
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
311
+ return true;
312
+ }
313
+ const payload = await triggerSystemAdminManualRebalance();
314
+ sendJson(req, res, 200, payload);
315
+ return true;
316
+ }
317
+
318
+ const groupActionMatch = subPath.match(/^\/groups\/([^/]+)\/(pin|unpin|failover)$/i);
319
+ if (groupActionMatch) {
320
+ if (!['POST'].includes(req.method || '')) {
321
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
322
+ return true;
323
+ }
324
+
325
+ const groupJid = decodePathSegment(groupActionMatch[1]);
326
+ const action = String(groupActionMatch[2] || '').toLowerCase();
327
+ const body = await readJsonBody(req).catch((error) => {
328
+ const statusCode = Number(error?.statusCode || 400);
329
+ sendJson(req, res, statusCode, { error: error?.message || 'Falha ao interpretar payload JSON.' });
330
+ return null;
331
+ });
332
+ if (body === null) return true;
333
+
334
+ if (action === 'pin' || action === 'unpin') {
335
+ const pinned = action === 'pin';
336
+ const payload = await setSystemAdminGroupPin({
337
+ groupJid,
338
+ pinned,
339
+ sessionId: body?.session_id || body?.sessionId || null,
340
+ reason: body?.reason || null,
341
+ changedBy: 'system_admin_api',
342
+ metadata: body?.metadata || null,
343
+ });
344
+ sendJson(req, res, 200, payload);
345
+ return true;
346
+ }
347
+
348
+ if (action === 'failover') {
349
+ const targetSessionId = String(body?.target_session_id || body?.targetSessionId || requestUrl.searchParams.get('target_session_id') || '')
350
+ .trim()
351
+ .slice(0, 64);
352
+ if (!targetSessionId) {
353
+ sendJson(req, res, 400, { error: 'target_session_id e obrigatorio.' });
354
+ return true;
355
+ }
356
+
357
+ const payload = await forceSystemAdminGroupFailover({
358
+ groupJid,
359
+ targetSessionId,
360
+ reason: body?.reason || 'admin_force_failover',
361
+ changedBy: 'system_admin_api',
362
+ metadata: body?.metadata || null,
363
+ });
364
+ sendJson(req, res, 200, payload);
365
+ return true;
366
+ }
367
+ }
368
+
369
+ sendJson(req, res, 404, { error: 'Endpoint de operacao multi-session nao encontrado.' });
370
+ return true;
371
+ };
372
+
127
373
  export const getSystemAdminRouteConfig = () => ({
128
374
  webPath: USER_SYSTEMADM_WEB_PATH,
129
375
  legacyWebPath: STICKER_ADMIN_WEB_PATH,
130
376
  apiAdminBasePath: SYSTEM_ADMIN_API_BASE_PATH,
131
377
  apiAdminSessionPath: SYSTEM_ADMIN_API_SESSION_PATH,
378
+ apiAdminMultiSessionPath: SYSTEM_ADMIN_MULTI_SESSION_API_PATH,
132
379
  legacyApiAdminBasePath: LEGACY_SYSTEM_ADMIN_API_BASE_PATH,
133
380
  legacyApiAdminSessionPath: LEGACY_SYSTEM_ADMIN_API_SESSION_PATH,
381
+ legacyApiAdminMultiSessionPath: LEGACY_SYSTEM_ADMIN_MULTI_SESSION_API_PATH,
134
382
  });
135
383
 
136
384
  export const maybeHandleSystemAdminRequest = async (req, res, { pathname, url }) => {
@@ -186,6 +434,25 @@ export const maybeHandleSystemAdminRequest = async (req, res, { pathname, url })
186
434
  }
187
435
 
188
436
  if (hasPathPrefix(pathname, SYSTEM_ADMIN_API_BASE_PATH) || hasPathPrefix(pathname, LEGACY_SYSTEM_ADMIN_API_BASE_PATH)) {
437
+ const multiSessionPath = mapMultiSessionApiPath(pathname);
438
+ if (multiSessionPath !== null) {
439
+ try {
440
+ return await handleMultiSessionOpsRequest(req, res, {
441
+ pathname: multiSessionPath,
442
+ url,
443
+ });
444
+ } catch (error) {
445
+ logger.error('Falha ao processar endpoint operacional multi-sessao.', {
446
+ action: 'system_admin_multi_session_endpoint_failed',
447
+ method: req.method,
448
+ path: pathname,
449
+ error: error?.message,
450
+ });
451
+ sendJson(req, res, 500, { error: 'Falha interna ao processar operacao multi-sessao.' });
452
+ return true;
453
+ }
454
+ }
455
+
189
456
  const legacyPathname = mapAdminApiPathToLegacy(pathname);
190
457
  if (!legacyPathname) return false;
191
458
 
@@ -1,3 +1,4 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
1
2
  import fs from 'node:fs/promises';
2
3
 
3
4
  export const createStickerCatalogSeoContext = ({ executeQuery, tables, listStickerPacksForCatalog, logger, sendJson, toSiteAbsoluteUrl, isPackPubliclyVisible, buildPackWebUrl, config }) => {
@@ -52,7 +53,7 @@ export const createStickerCatalogSeoContext = ({ executeQuery, tables, listStick
52
53
  const buildStickersReactBundleUrl = () => appendAssetVersionQuery('/assets/js/stickers-react.bundle.js');
53
54
 
54
55
  const buildCatalogDiscoveryLinksHtml = async () => {
55
- if (SEO_DISCOVERY_CACHE.expiresAt > Date.now() && SEO_DISCOVERY_CACHE.html) {
56
+ if (SEO_DISCOVERY_CACHE.expiresAt > __timeNowMs() && SEO_DISCOVERY_CACHE.html) {
56
57
  return SEO_DISCOVERY_CACHE.html;
57
58
  }
58
59
 
@@ -67,7 +68,7 @@ export const createStickerCatalogSeoContext = ({ executeQuery, tables, listStick
67
68
  const links = (Array.isArray(packs) ? packs : []).filter((pack) => pack?.pack_key && isPackPubliclyVisible(pack)).slice(0, seoDiscoveryLinkLimit);
68
69
 
69
70
  if (!links.length) {
70
- SEO_DISCOVERY_CACHE.expiresAt = Date.now() + seoDiscoveryCacheSeconds * 1000;
71
+ SEO_DISCOVERY_CACHE.expiresAt = __timeNowMs() + seoDiscoveryCacheSeconds * 1000;
71
72
  SEO_DISCOVERY_CACHE.html = '';
72
73
  return '';
73
74
  }
@@ -91,7 +92,7 @@ export const createStickerCatalogSeoContext = ({ executeQuery, tables, listStick
91
92
  </section>
92
93
  </noscript>`;
93
94
 
94
- SEO_DISCOVERY_CACHE.expiresAt = Date.now() + seoDiscoveryCacheSeconds * 1000;
95
+ SEO_DISCOVERY_CACHE.expiresAt = __timeNowMs() + seoDiscoveryCacheSeconds * 1000;
95
96
  SEO_DISCOVERY_CACHE.html = html;
96
97
  return html;
97
98
  } catch (error) {
@@ -116,7 +117,7 @@ export const createStickerCatalogSeoContext = ({ executeQuery, tables, listStick
116
117
  __INITIAL_PACK_KEY__: escapeHtmlAttribute(initialPackKey || ''),
117
118
  __CATALOG_STYLES_PATH__: escapeHtmlAttribute(buildCatalogStylesUrl()),
118
119
  __CATALOG_SCRIPT_PATH__: escapeHtmlAttribute(buildCatalogScriptUrl()),
119
- __CURRENT_YEAR__: String(new Date().getFullYear()),
120
+ __CURRENT_YEAR__: String(__timeNow().getFullYear()),
120
121
  };
121
122
 
122
123
  let html = template;
@@ -150,7 +151,7 @@ export const createStickerCatalogSeoContext = ({ executeQuery, tables, listStick
150
151
  const coverUrl = toSiteAbsoluteUrl(packSummary?.cover_url || fallbackCoverUrl);
151
152
  const publisher = truncateText(packSummary?.publisher || 'Criador OmniZap', 80);
152
153
  const stickerCount = Math.max(0, Number(packSummary?.sticker_count || 0));
153
- const updatedAt = packSummary?.updated_at || packSummary?.created_at || new Date().toISOString();
154
+ const updatedAt = packSummary?.updated_at || packSummary?.created_at || __timeNowIso();
154
155
  const schemaJson = JSON.stringify(
155
156
  {
156
157
  '@context': 'https://schema.org',
@@ -243,7 +244,7 @@ export const createStickerCatalogSeoContext = ({ executeQuery, tables, listStick
243
244
 
244
245
  <meta property="og:type" content="website" />
245
246
  <meta property="og:locale" content="pt_BR" />
246
- <meta property="og:site_name" content="OmniZap System" />
247
+ <meta property="og:site_name" content="Omnizap" />
247
248
  <meta property="og:title" content="${escapeHtmlAttribute(packName)}" />
248
249
  <meta property="og:description" content="${escapeHtmlAttribute(packDescription)}" />
249
250
  <meta property="og:url" content="${escapeHtmlAttribute(canonicalUrl)}" />
@@ -337,7 +338,7 @@ export const createStickerCatalogSeoContext = ({ executeQuery, tables, listStick
337
338
  __STICKER_LOGIN_WEB_PATH__: escapeHtmlAttribute(stickerLoginWebPath),
338
339
  __STICKER_API_BASE_PATH__: escapeHtmlAttribute(stickerApiBasePath),
339
340
  __PACK_COMMAND_PREFIX__: escapeHtmlAttribute(packCommandPrefix),
340
- __CURRENT_YEAR__: String(new Date().getFullYear()),
341
+ __CURRENT_YEAR__: String(__timeNow().getFullYear()),
341
342
  };
342
343
 
343
344
  let html = template;
@@ -348,7 +349,7 @@ export const createStickerCatalogSeoContext = ({ executeQuery, tables, listStick
348
349
  };
349
350
 
350
351
  const buildSitemapXml = async () => {
351
- if (SITEMAP_CACHE.expiresAt > Date.now() && SITEMAP_CACHE.xml) {
352
+ if (SITEMAP_CACHE.expiresAt > __timeNowMs() && SITEMAP_CACHE.xml) {
352
353
  return SITEMAP_CACHE.xml;
353
354
  }
354
355
 
@@ -441,7 +442,7 @@ export const createStickerCatalogSeoContext = ({ executeQuery, tables, listStick
441
442
  .join('\n');
442
443
 
443
444
  const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${xmlItems}\n</urlset>\n`;
444
- SITEMAP_CACHE.expiresAt = Date.now() + sitemapCacheSeconds * 1000;
445
+ SITEMAP_CACHE.expiresAt = __timeNowMs() + sitemapCacheSeconds * 1000;
445
446
  SITEMAP_CACHE.xml = xml;
446
447
  return xml;
447
448
  };
@@ -1,3 +1,4 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
1
2
  import { withTimeout } from '../../http/httpRequestUtils.js';
2
3
 
3
4
  export const createStickerCatalogNonCatalogHandlers = ({ sendJson, sendText, logger, getSystemSummaryCached, systemSummaryCache, systemSummaryCacheSeconds, getReadmeSummaryCached, readmeSummaryCache, readmeSummaryCacheSeconds, getGlobalRankingSummaryCached, globalRankRefreshSeconds, globalRankCache, sanitizeRankingPayloadByBot, getActiveSocket, resolveBotUserCandidates, getMarketplaceGlobalStatsCached, marketplaceGlobalStatsCacheSeconds, marketplaceGlobalStatsCache, githubRepoInfo, githubProjectCacheSeconds, fetchGitHubProjectSummary, buildSupportInfo, buildBotContactInfo, getMarketplaceStatsCached, resolveGoogleWebSessionFromRequest, mapGoogleSessionResponseData, isAuthenticatedGoogleSession, stickerWebGoogleClientId, homeBootstrapExposeContact, trackWebVisitMetric, resolveVisitPathFromReferrer, normalizeCatalogVisibility }) => {
@@ -18,7 +19,7 @@ export const createStickerCatalogNonCatalogHandlers = ({ sendJson, sendText, log
18
19
  total_users: totalUsers,
19
20
  total_messages: totalMessages,
20
21
  total_commands: totalCommands,
21
- updated_at: new Date().toISOString(),
22
+ updated_at: __timeNowIso(),
22
23
  };
23
24
 
24
25
  if (systemLatencyMs !== null) {
@@ -1,3 +1,4 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
1
2
  import fs from 'node:fs/promises';
2
3
  import path from 'node:path';
3
4
  import { createHash, randomUUID, timingSafeEqual } from 'node:crypto';
@@ -66,6 +67,7 @@ export const stripWebpExtension = (value) =>
66
67
  .replace(/\.webp$/i, '');
67
68
 
68
69
  const clampInt = (value, fallback, min, max) => {
70
+ if (value === undefined || value === null || value === '') return fallback;
69
71
  const parsed = Number(value);
70
72
  if (!Number.isFinite(parsed)) return fallback;
71
73
  return Math.max(min, Math.min(max, Math.floor(parsed)));
@@ -218,7 +220,7 @@ const getCacheBucket = (cacheMap, key) => {
218
220
 
219
221
  const getCachedSnapshot = async ({ cacheMap, key, ttlSeconds, staleWhileRefresh = true, staleOnError = true, load }) => {
220
222
  const bucket = getCacheBucket(cacheMap, key);
221
- const now = Date.now();
223
+ const now = __timeNowMs();
222
224
  const hasValue = bucket.value !== null;
223
225
  const hasFreshValue = hasValue && now < bucket.expiresAt;
224
226
 
@@ -231,7 +233,7 @@ const getCachedSnapshot = async ({ cacheMap, key, ttlSeconds, staleWhileRefresh
231
233
  .then(load)
232
234
  .then((value) => {
233
235
  bucket.value = value;
234
- bucket.expiresAt = Date.now() + ttlSeconds * 1000;
236
+ bucket.expiresAt = __timeNowMs() + ttlSeconds * 1000;
235
237
  return value;
236
238
  })
237
239
  .finally(() => {
@@ -598,10 +600,10 @@ const buildPackPublishStateData = async (pack, { includeUploads = true, connecti
598
600
 
599
601
  const maybeCleanupStaleDraftPacks = async () => {
600
602
  if (staleDraftCleanupState.running) return;
601
- if (Date.now() - staleDraftCleanupState.lastRunAt < WEB_DRAFT_CLEANUP_RUN_INTERVAL_MS) return;
603
+ if (__timeNowMs() - staleDraftCleanupState.lastRunAt < WEB_DRAFT_CLEANUP_RUN_INTERVAL_MS) return;
602
604
 
603
605
  staleDraftCleanupState.running = true;
604
- staleDraftCleanupState.lastRunAt = Date.now();
606
+ staleDraftCleanupState.lastRunAt = __timeNowMs();
605
607
 
606
608
  try {
607
609
  const rows = await executeQuery(
@@ -708,7 +710,7 @@ const saveWebPackEditToken = ({ packId, ownerJid }) => {
708
710
  webPackEditTokenMap.set(token, {
709
711
  packId,
710
712
  ownerJid,
711
- expiresAt: Date.now() + PACK_WEB_EDIT_TOKEN_TTL_MS,
713
+ expiresAt: __timeNowMs() + PACK_WEB_EDIT_TOKEN_TTL_MS,
712
714
  });
713
715
  return token;
714
716
  };
@@ -718,7 +720,7 @@ const resolveWebPackEditToken = (token) => {
718
720
  if (!normalized) return null;
719
721
  const entry = webPackEditTokenMap.get(normalized);
720
722
  if (!entry) return null;
721
- if (entry.expiresAt <= Date.now()) {
723
+ if (entry.expiresAt <= __timeNowMs()) {
722
724
  webPackEditTokenMap.delete(normalized);
723
725
  return null;
724
726
  }
@@ -786,7 +788,7 @@ const isPreviewVariantRequested = (url) => {
786
788
  const getStickerPreviewFromCache = (cacheKey) => {
787
789
  const entry = STICKER_PREVIEW_CACHE.get(cacheKey);
788
790
  if (!entry) return null;
789
- if (entry.expiresAt <= Date.now()) {
791
+ if (entry.expiresAt <= __timeNowMs()) {
790
792
  STICKER_PREVIEW_CACHE.delete(cacheKey);
791
793
  return null;
792
794
  }
@@ -806,7 +808,7 @@ const saveStickerPreviewToCache = (cacheKey, buffer) => {
806
808
  }
807
809
  STICKER_PREVIEW_CACHE.set(cacheKey, {
808
810
  buffer,
809
- expiresAt: Date.now() + STICKER_PREVIEW_CACHE_TTL_MS,
811
+ expiresAt: __timeNowMs() + STICKER_PREVIEW_CACHE_TTL_MS,
810
812
  });
811
813
  };
812
814
 
@@ -963,8 +965,6 @@ const convertUploadMediaToWebp = async ({ ownerJid, buffer, mimetype }) => {
963
965
  const PACK_TAG_MARKER_REGEX = /\[pack-tags:([^\]]+)\]/i;
964
966
  const AUTO_PACK_MARKER_REGEX = /\[(?:auto-theme|auto-tag):[^\]]+\]/gi;
965
967
  const AUTO_PACK_MARKER_TEST_REGEX = /\[(?:auto-theme|auto-tag):[^\]]+\]/i;
966
- const AUTO_PACK_COLLECTOR_MARKER = '[auto-pack:collector]';
967
- const AUTO_PACK_COLLECTOR_LEGACY_TEXT = 'coleção automática de figurinhas criadas pelo usuário.';
968
968
  const AUTO_PACK_DESCRIPTION_PREFIX_REGEX = /^curadoria automática por tema\.\s*tema:\s*[^.]+\.?\s*(?:score\s*=\s*-?\d+(?:\.\d+)?\.?\s*)?/i;
969
969
  const AUTO_PACK_SCORE_FRAGMENT_REGEX = /\bscore\s*=\s*-?\d+(?:\.\d+)?\.?/gi;
970
970
  const normalizePackTag = (value) =>
@@ -1023,29 +1023,10 @@ const parsePackDescriptionMetadata = (description) => {
1023
1023
  };
1024
1024
  };
1025
1025
 
1026
- const isCollectorAutoPack = (pack) => {
1027
- if (!pack || typeof pack !== 'object') return false;
1028
- const description = String(pack.description || '').toLowerCase();
1029
- return description.includes(AUTO_PACK_COLLECTOR_MARKER) || description.includes(AUTO_PACK_COLLECTOR_LEGACY_TEXT);
1030
- };
1031
-
1032
- const isThemeCurationAutoPack = (pack) => {
1033
- if (!pack || typeof pack !== 'object') return false;
1034
- const name = String(pack.name || '').trim();
1035
- if (/^\[auto\]/i.test(name)) return true;
1036
-
1037
- const description = String(pack.description || '').toLowerCase();
1038
- if (description.includes('[auto-theme:') || description.includes('[auto-tag:')) return true;
1039
-
1040
- return Boolean(String(pack.pack_theme_key || '').trim());
1041
- };
1042
-
1043
1026
  const shouldHidePackFromMyProfileDefault = (pack, { includeAutoPacks = false } = {}) => {
1044
1027
  if (!pack || typeof pack !== 'object') return false;
1045
1028
  if (includeAutoPacks) return false;
1046
- if (isCollectorAutoPack(pack)) return false;
1047
- if (isThemeCurationAutoPack(pack)) return true;
1048
- return pack.is_auto_pack === true || Number(pack.is_auto_pack || 0) === 1;
1029
+ return false;
1049
1030
  };
1050
1031
 
1051
1032
  const buildPackDescriptionWithTags = (description, tags = []) => {
@@ -1719,7 +1700,7 @@ const buildMarketplaceStatsSnapshot = async (visibility) => {
1719
1700
  const getMarketplaceStatsCached = async (visibility) => {
1720
1701
  const normalizedVisibility = normalizeCatalogVisibility(visibility);
1721
1702
  const bucket = getHomeMarketplaceStatsCacheBucket(normalizedVisibility);
1722
- const now = Date.now();
1703
+ const now = __timeNowMs();
1723
1704
  const hasValue = Boolean(bucket.value);
1724
1705
 
1725
1706
  if (hasValue && now < bucket.expiresAt) {
@@ -1730,9 +1711,15 @@ const getMarketplaceStatsCached = async (visibility) => {
1730
1711
  bucket.pending = withTimeout(buildMarketplaceStatsSnapshot(normalizedVisibility), 5000)
1731
1712
  .then((data) => {
1732
1713
  bucket.value = data;
1733
- bucket.expiresAt = Date.now() + HOME_MARKETPLACE_STATS_CACHE_SECONDS * 1000;
1714
+ bucket.expiresAt = __timeNowMs() + HOME_MARKETPLACE_STATS_CACHE_SECONDS * 1000;
1734
1715
  return data;
1735
1716
  })
1717
+ .catch((error) => {
1718
+ if (hasValue && bucket.value) {
1719
+ return bucket.value;
1720
+ }
1721
+ throw error;
1722
+ })
1736
1723
  .finally(() => {
1737
1724
  bucket.pending = null;
1738
1725
  });
@@ -2485,7 +2472,7 @@ const handleManagedPackRequest = async (req, res, packKey) => {
2485
2472
  deleted: true,
2486
2473
  pack_key: result?.deletedPack?.pack_key || normalizedPackKey,
2487
2474
  id: result?.deletedPack?.id || context.pack?.id || null,
2488
- deleted_at: toIsoOrNull(result?.deletedPack?.deleted_at || new Date()),
2475
+ deleted_at: toIsoOrNull(result?.deletedPack?.deleted_at || __timeNow()),
2489
2476
  removed_sticker_count: Number(result?.removedCount || 0),
2490
2477
  });
2491
2478
  } catch (error) {
@@ -3218,7 +3205,7 @@ const handleCreatePackRequest = async (req, res) => {
3218
3205
  });
3219
3206
  const manualTags = mergeUniqueTags(Array.isArray(payload?.tags) ? payload.tags : []).slice(0, 8);
3220
3207
  const persistedDescription = buildPackDescriptionWithTags(description, manualTags);
3221
- const visibility = String(payload?.visibility || 'public')
3208
+ const visibility = String(payload?.visibility || 'private')
3222
3209
  .trim()
3223
3210
  .toLowerCase();
3224
3211
  const googleSession = await resolveGoogleWebSessionFromRequest(req);
@@ -3468,7 +3455,7 @@ const handleUploadStickerToPackRequest = async (req, res, packKey) => {
3468
3455
  source_mimetype: decoded.mimetype || 'image/webp',
3469
3456
  upload_status: 'processing',
3470
3457
  attempt_count: 1,
3471
- last_attempt_at: new Date(),
3458
+ last_attempt_at: __timeNow(),
3472
3459
  },
3473
3460
  connection,
3474
3461
  );
@@ -3481,7 +3468,7 @@ const handleUploadStickerToPackRequest = async (req, res, packKey) => {
3481
3468
  error_code: null,
3482
3469
  error_message: null,
3483
3470
  attempt_count: Math.max(1, Number(existingUpload.attempt_count || 0) + 1),
3484
- last_attempt_at: new Date(),
3471
+ last_attempt_at: __timeNow(),
3485
3472
  },
3486
3473
  connection,
3487
3474
  );
@@ -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,3 +1,4 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
1
2
  import logger from '#logger';
2
3
  import { toIsoOrNull } from '../../http/httpRequestUtils.js';
3
4
 
@@ -33,7 +34,7 @@ const githubFetchJson = async (url) => {
33
34
 
34
35
  const headers = {
35
36
  Accept: 'application/vnd.github+json',
36
- 'User-Agent': 'omnizap-system/2.1',
37
+ 'User-Agent': 'omnizap/2.1',
37
38
  };
38
39
 
39
40
  if (GITHUB_TOKEN) {
@@ -109,7 +110,7 @@ export const fetchGitHubProjectSummary = async () => {
109
110
  throw new Error('GITHUB_REPOSITORY invalido');
110
111
  }
111
112
 
112
- const now = Date.now();
113
+ const now = __timeNowMs();
113
114
  if (GITHUB_PROJECT_CACHE.value && now < GITHUB_PROJECT_CACHE.expiresAt) {
114
115
  return GITHUB_PROJECT_CACHE.value;
115
116
  }