@omnizap-system/omnizap 2.5.12

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 (425) hide show
  1. package/.clusterfuzzlite/Dockerfile +10 -0
  2. package/.env.example +907 -0
  3. package/.github/codeql/codeql-config.yml +10 -0
  4. package/.github/dependabot.yml +35 -0
  5. package/.github/workflows/ci.yml +73 -0
  6. package/.github/workflows/codeql.yml +106 -0
  7. package/.github/workflows/db-migration-check.yml +98 -0
  8. package/.github/workflows/dependency-review.yml +22 -0
  9. package/.github/workflows/deploy.yml +95 -0
  10. package/.github/workflows/release.yml +106 -0
  11. package/.github/workflows/security-attest-provenance.yml +51 -0
  12. package/.github/workflows/security-gitleaks.yml +34 -0
  13. package/.github/workflows/security-runner-hardening.yml +31 -0
  14. package/.github/workflows/security-scorecard.yml +44 -0
  15. package/.github/workflows/security-zap-baseline.yml +44 -0
  16. package/.github/workflows/security-zap-full-scan.yml +43 -0
  17. package/.github/workflows/security-zizmor.yml +36 -0
  18. package/.github/workflows/wiki-sync.yml +44 -0
  19. package/.gitleaks.toml +15 -0
  20. package/.prettierrc +34 -0
  21. package/CODE_OF_CONDUCT.md +114 -0
  22. package/LICENSE +56 -0
  23. package/README.md +110 -0
  24. package/SECURITY.md +110 -0
  25. package/app/config/index.js +4 -0
  26. package/app/configParts/adminIdentity.js +92 -0
  27. package/app/configParts/baileysConfig.js +1818 -0
  28. package/app/configParts/groupUtils.js +692 -0
  29. package/app/configParts/loggerConfig.js +394 -0
  30. package/app/configParts/messagePersistenceService.js +305 -0
  31. package/app/connection/baileysCompatibility.test.js +40 -0
  32. package/app/connection/baileysDbAuthState.js +344 -0
  33. package/app/connection/socketController.js +2243 -0
  34. package/app/controllers/messageController.js +7 -0
  35. package/app/controllers/messagePipeline/commandMiddleware.js +146 -0
  36. package/app/controllers/messagePipeline/conversationMiddleware.js +183 -0
  37. package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +522 -0
  38. package/app/controllers/messagePipeline/postProcessingMiddleware.js +41 -0
  39. package/app/controllers/messagePipeline/preProcessingMiddlewares.js +166 -0
  40. package/app/controllers/messageProcessingPipeline.js +699 -0
  41. package/app/modules/adminModule/AGENT.md +4056 -0
  42. package/app/modules/adminModule/adminAiHelpService.js +56 -0
  43. package/app/modules/adminModule/adminConfigRuntime.js +177 -0
  44. package/app/modules/adminModule/commandConfig.json +7122 -0
  45. package/app/modules/adminModule/groupCommandHandlers.js +1823 -0
  46. package/app/modules/adminModule/groupCommandHandlers.test.js +350 -0
  47. package/app/modules/adminModule/groupEventHandlers.js +399 -0
  48. package/app/modules/aiModule/AGENT.md +547 -0
  49. package/app/modules/aiModule/aiAiHelpService.js +14 -0
  50. package/app/modules/aiModule/aiConfigRuntime.js +135 -0
  51. package/app/modules/aiModule/catCommand.js +967 -0
  52. package/app/modules/aiModule/commandConfig.json +981 -0
  53. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +83 -0
  54. package/app/modules/gameModule/AGENT.md +196 -0
  55. package/app/modules/gameModule/commandConfig.json +366 -0
  56. package/app/modules/gameModule/diceCommand.js +42 -0
  57. package/app/modules/gameModule/gameAiHelpService.js +14 -0
  58. package/app/modules/gameModule/gameConfigRuntime.js +68 -0
  59. package/app/modules/menuModule/AGENT.md +205 -0
  60. package/app/modules/menuModule/commandConfig.json +366 -0
  61. package/app/modules/menuModule/common.js +316 -0
  62. package/app/modules/menuModule/menuAiHelpService.js +14 -0
  63. package/app/modules/menuModule/menuConfigRuntime.js +68 -0
  64. package/app/modules/menuModule/menus.js +66 -0
  65. package/app/modules/playModule/AGENT.md +321 -0
  66. package/app/modules/playModule/commandConfig.json +584 -0
  67. package/app/modules/playModule/playAiHelpService.js +14 -0
  68. package/app/modules/playModule/playCommand.js +1417 -0
  69. package/app/modules/playModule/playConfigRuntime.js +68 -0
  70. package/app/modules/quoteModule/AGENT.md +199 -0
  71. package/app/modules/quoteModule/commandConfig.json +366 -0
  72. package/app/modules/quoteModule/quoteAiHelpService.js +14 -0
  73. package/app/modules/quoteModule/quoteCommand.js +842 -0
  74. package/app/modules/quoteModule/quoteConfigRuntime.js +68 -0
  75. package/app/modules/rpgPokemonModule/AGENT.md +229 -0
  76. package/app/modules/rpgPokemonModule/commandConfig.json +386 -0
  77. package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +795 -0
  78. package/app/modules/rpgPokemonModule/rpgBattleService.js +2110 -0
  79. package/app/modules/rpgPokemonModule/rpgBattleService.test.js +770 -0
  80. package/app/modules/rpgPokemonModule/rpgEvolutionUtils.js +22 -0
  81. package/app/modules/rpgPokemonModule/rpgPokemonAiHelpService.js +14 -0
  82. package/app/modules/rpgPokemonModule/rpgPokemonCommand.js +174 -0
  83. package/app/modules/rpgPokemonModule/rpgPokemonConfigRuntime.js +68 -0
  84. package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +192 -0
  85. package/app/modules/rpgPokemonModule/rpgPokemonDomain.test.js +93 -0
  86. package/app/modules/rpgPokemonModule/rpgPokemonEvolution.test.js +46 -0
  87. package/app/modules/rpgPokemonModule/rpgPokemonMessages.js +746 -0
  88. package/app/modules/rpgPokemonModule/rpgPokemonRepository.js +1847 -0
  89. package/app/modules/rpgPokemonModule/rpgPokemonService.js +6839 -0
  90. package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +354 -0
  91. package/app/modules/statsModule/AGENT.md +320 -0
  92. package/app/modules/statsModule/commandConfig.json +540 -0
  93. package/app/modules/statsModule/globalRankingCommand.js +64 -0
  94. package/app/modules/statsModule/rankingCommand.js +41 -0
  95. package/app/modules/statsModule/rankingCommon.js +1305 -0
  96. package/app/modules/statsModule/statsAiHelpService.js +14 -0
  97. package/app/modules/statsModule/statsConfigRuntime.js +68 -0
  98. package/app/modules/stickerModule/AGENT.md +692 -0
  99. package/app/modules/stickerModule/addStickerMetadata.js +239 -0
  100. package/app/modules/stickerModule/commandConfig.json +1216 -0
  101. package/app/modules/stickerModule/convertToWebp.js +367 -0
  102. package/app/modules/stickerModule/stickerAiHelpService.js +14 -0
  103. package/app/modules/stickerModule/stickerCommand.js +446 -0
  104. package/app/modules/stickerModule/stickerConfigRuntime.js +68 -0
  105. package/app/modules/stickerModule/stickerConvertCommand.js +159 -0
  106. package/app/modules/stickerModule/stickerTextCommand.js +653 -0
  107. package/app/modules/stickerPackModule/AGENT.md +215 -0
  108. package/app/modules/stickerPackModule/autoPackCollectorRuntime.js +20 -0
  109. package/app/modules/stickerPackModule/autoPackCollectorService.js +357 -0
  110. package/app/modules/stickerPackModule/commandConfig.json +387 -0
  111. package/app/modules/stickerPackModule/domainEventOutboxRepository.js +227 -0
  112. package/app/modules/stickerPackModule/domainEvents.js +52 -0
  113. package/app/modules/stickerPackModule/semanticReclassificationEngine.js +429 -0
  114. package/app/modules/stickerPackModule/semanticReclassificationEngine.test.js +75 -0
  115. package/app/modules/stickerPackModule/semanticThemeClusterService.js +544 -0
  116. package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +400 -0
  117. package/app/modules/stickerPackModule/stickerAssetRepository.js +400 -0
  118. package/app/modules/stickerPackModule/stickerAssetReprocessQueueRepository.js +175 -0
  119. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +3702 -0
  120. package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +559 -0
  121. package/app/modules/stickerPackModule/stickerClassificationService.js +557 -0
  122. package/app/modules/stickerPackModule/stickerDedicatedTaskWorkerRuntime.js +249 -0
  123. package/app/modules/stickerPackModule/stickerDomainEventBus.js +65 -0
  124. package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +208 -0
  125. package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +99 -0
  126. package/app/modules/stickerPackModule/stickerObjectStorageService.js +285 -0
  127. package/app/modules/stickerPackModule/stickerPackAiHelpService.js +14 -0
  128. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +1148 -0
  129. package/app/modules/stickerPackModule/stickerPackConfigRuntime.js +68 -0
  130. package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +152 -0
  131. package/app/modules/stickerPackModule/stickerPackErrors.js +30 -0
  132. package/app/modules/stickerPackModule/stickerPackInteractionEventRepository.js +101 -0
  133. package/app/modules/stickerPackModule/stickerPackItemRepository.js +432 -0
  134. package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +313 -0
  135. package/app/modules/stickerPackModule/stickerPackMessageService.js +268 -0
  136. package/app/modules/stickerPackModule/stickerPackRepository.js +450 -0
  137. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRepository.js +179 -0
  138. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +271 -0
  139. package/app/modules/stickerPackModule/stickerPackService.js +733 -0
  140. package/app/modules/stickerPackModule/stickerPackServiceRuntime.js +32 -0
  141. package/app/modules/stickerPackModule/stickerPackUtils.js +107 -0
  142. package/app/modules/stickerPackModule/stickerStorageService.js +559 -0
  143. package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +242 -0
  144. package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +242 -0
  145. package/app/modules/systemMetricsModule/AGENT.md +193 -0
  146. package/app/modules/systemMetricsModule/commandConfig.json +344 -0
  147. package/app/modules/systemMetricsModule/pingCommand.js +399 -0
  148. package/app/modules/systemMetricsModule/systemMetricsAiHelpService.js +14 -0
  149. package/app/modules/systemMetricsModule/systemMetricsConfigRuntime.js +68 -0
  150. package/app/modules/tiktokModule/AGENT.md +196 -0
  151. package/app/modules/tiktokModule/commandConfig.json +366 -0
  152. package/app/modules/tiktokModule/tiktokAiHelpService.js +14 -0
  153. package/app/modules/tiktokModule/tiktokCommand.js +716 -0
  154. package/app/modules/tiktokModule/tiktokConfigRuntime.js +68 -0
  155. package/app/modules/userModule/AGENT.md +200 -0
  156. package/app/modules/userModule/commandConfig.json +386 -0
  157. package/app/modules/userModule/userAiHelpService.js +14 -0
  158. package/app/modules/userModule/userCommand.js +1155 -0
  159. package/app/modules/userModule/userConfigRuntime.js +68 -0
  160. package/app/modules/waifuPicsModule/AGENT.md +431 -0
  161. package/app/modules/waifuPicsModule/commandConfig.json +780 -0
  162. package/app/modules/waifuPicsModule/waifuPicsAiHelpService.js +14 -0
  163. package/app/modules/waifuPicsModule/waifuPicsCommand.js +586 -0
  164. package/app/modules/waifuPicsModule/waifuPicsConfigRuntime.js +68 -0
  165. package/app/observability/metrics.js +766 -0
  166. package/app/services/ai/aiHelpResponseCacheRepository.js +280 -0
  167. package/app/services/ai/aiLearningRepository.js +400 -0
  168. package/app/services/ai/commandConfigEnrichmentRepository.js +769 -0
  169. package/app/services/ai/commandConfigEnrichmentService.js +452 -0
  170. package/app/services/ai/commandConfigValidationService.js +443 -0
  171. package/app/services/ai/commandToolBuilderService.js +192 -0
  172. package/app/services/ai/conversationRouterService.js +516 -0
  173. package/app/services/ai/geminiService.js +115 -0
  174. package/app/services/ai/geminiService.test.js +87 -0
  175. package/app/services/ai/globalModuleAiHelpService.js +1412 -0
  176. package/app/services/ai/globalToolCallingService.js +203 -0
  177. package/app/services/ai/messageCommandExecutionService.js +391 -0
  178. package/app/services/ai/moduleAiHelpCoreService.js +1099 -0
  179. package/app/services/ai/moduleAiHelpWrapperFactory.js +65 -0
  180. package/app/services/ai/moduleCommandConfigRuntimeService.js +113 -0
  181. package/app/services/ai/moduleToolExecutorService.js +464 -0
  182. package/app/services/ai/moduleToolRegistryService.js +178 -0
  183. package/app/services/ai/toolCandidateSelectorService.js +781 -0
  184. package/app/services/auth/googleWebLinkService.js +80 -0
  185. package/app/services/auth/whatsappLoginLinkService.js +230 -0
  186. package/app/services/external/pokeApiService.js +398 -0
  187. package/app/services/group/groupMetadataService.js +311 -0
  188. package/app/services/infra/dbWriteQueue.js +874 -0
  189. package/app/services/infra/featureFlagService.js +131 -0
  190. package/app/services/infra/queueUtils.js +55 -0
  191. package/app/services/messaging/captchaService.js +491 -0
  192. package/app/services/messaging/messagePersistenceService.js +1 -0
  193. package/app/services/messaging/newsBroadcastService.js +347 -0
  194. package/app/services/sticker/stickerFocusService.js +347 -0
  195. package/app/services/sticker/stickerFocusService.test.js +43 -0
  196. package/app/store/aiPromptStore.js +38 -0
  197. package/app/store/conversationSessionStore.js +131 -0
  198. package/app/store/groupConfigStore.js +58 -0
  199. package/app/store/premiumUserStore.js +54 -0
  200. package/app/utils/antiLink/antiLinkModule.js +700 -0
  201. package/app/utils/http/getImageBufferModule.js +18 -0
  202. package/app/utils/json/jsonSanitizer.js +113 -0
  203. package/app/utils/json/jsonSanitizer.test.js +40 -0
  204. package/app/utils/systemMetrics/systemMetricsModule.js +88 -0
  205. package/app/workers/aiLearningWorker.js +605 -0
  206. package/app/workers/commandConfigEnrichmentWorker.js +242 -0
  207. package/database/index.js +2075 -0
  208. package/database/init.js +151 -0
  209. package/database/migrations/.gitkeep +0 -0
  210. package/database/migrations/20260307_d0_hardening_down.sql +64 -0
  211. package/database/migrations/20260307_d0_hardening_up.sql +79 -0
  212. package/database/migrations/20260307_d1_terms_acceptance_down.sql +11 -0
  213. package/database/migrations/20260307_d1_terms_acceptance_up.sql +37 -0
  214. package/database/migrations/20260307_d2_auth_hardening_down.sql +75 -0
  215. package/database/migrations/20260307_d2_auth_hardening_up.sql +100 -0
  216. package/database/migrations/20260314_d7_canonical_sender_down.sql +53 -0
  217. package/database/migrations/20260314_d7_canonical_sender_up.sql +114 -0
  218. package/database/migrations/20260406_d30_security_analytics_down.sql +95 -0
  219. package/database/migrations/20260406_d30_security_analytics_up.sql +292 -0
  220. package/database/migrations/20260407_d31_web_google_session_token_hardening_down.sql +2 -0
  221. package/database/migrations/20260407_d31_web_google_session_token_hardening_up.sql +17 -0
  222. package/database/migrations/20260408_d32_ai_help_response_cache_down.sql +1 -0
  223. package/database/migrations/20260408_d32_ai_help_response_cache_up.sql +22 -0
  224. package/database/migrations/20260409_d33_ai_learning_tables_down.sql +4 -0
  225. package/database/migrations/20260409_d33_ai_learning_tables_up.sql +52 -0
  226. package/database/migrations/20260410_d34_command_config_enrichment_down.sql +3 -0
  227. package/database/migrations/20260410_d34_command_config_enrichment_up.sql +48 -0
  228. package/database/schema.sql +1186 -0
  229. package/docker-compose.yml +104 -0
  230. package/docs/audits/stickerCatalogController-out-of-scope.md +103 -0
  231. package/docs/audits/stickerCatalogController-symbols.md +58 -0
  232. package/docs/compliance/acceptable-use-policy-2026-03-07.md +35 -0
  233. package/docs/compliance/dpa-b2b-standard-2026-03-07.md +80 -0
  234. package/docs/compliance/monthly-compliance-checklist-2026-03-07.md +88 -0
  235. package/docs/compliance/notice-and-takedown-policy-2026-03-07.md +34 -0
  236. package/docs/compliance/privacy-policy-2026-03-07.md +75 -0
  237. package/docs/compliance/subprocessors-inventory-2026-03-07.md +16 -0
  238. package/docs/database/production-db-evolution-runbook-2026q1.md +365 -0
  239. package/docs/security/dsar-lgpd-runbook-2026-03-07.md +86 -0
  240. package/docs/security/incident-response-lgpd-anpd-runbook-2026-03-07.md +77 -0
  241. package/docs/security/network-hardening-runbook-2026-03-07.md +137 -0
  242. package/docs/seo/omnizap-seo-playbook-br-2026-02-28.md +238 -0
  243. package/docs/seo/satellite-page-template.md +116 -0
  244. package/docs/seo/satellite-pages-phase1.json +364 -0
  245. package/docs/wiki/Home.md +120 -0
  246. package/docs/wiki/pair-extraordinaire-2026-03-08.md +3 -0
  247. package/docs/wiki/recent-changes-2026-03-08.md +47 -0
  248. package/ecosystem.prod.config.cjs +135 -0
  249. package/eslint.config.js +89 -0
  250. package/index.js +488 -0
  251. package/ml/clip_classifier/Dockerfile +18 -0
  252. package/ml/clip_classifier/README.md +118 -0
  253. package/ml/clip_classifier/adaptive_scoring.py +40 -0
  254. package/ml/clip_classifier/classifier.py +654 -0
  255. package/ml/clip_classifier/embedding_store.py +481 -0
  256. package/ml/clip_classifier/env_loader.py +15 -0
  257. package/ml/clip_classifier/llm_label_expander.py +144 -0
  258. package/ml/clip_classifier/main.py +213 -0
  259. package/ml/clip_classifier/requirements.txt +10 -0
  260. package/ml/clip_classifier/similarity_engine.py +74 -0
  261. package/new-logo.png +0 -0
  262. package/observability/alert-rules.yml +60 -0
  263. package/observability/grafana/dashboards/omnizap-mysql.json +136 -0
  264. package/observability/grafana/dashboards/omnizap-overview.json +170 -0
  265. package/observability/grafana/provisioning/dashboards/dashboards.yml +11 -0
  266. package/observability/grafana/provisioning/datasources/datasources.yml +15 -0
  267. package/observability/loki-config.yml +38 -0
  268. package/observability/mysql-setup.sql +46 -0
  269. package/observability/prometheus.yml +35 -0
  270. package/observability/promtail-config.yml +84 -0
  271. package/observability/sticker-catalog-slo.md +83 -0
  272. package/observability/sticker-scale-hardening-rollout.md +128 -0
  273. package/package.json +144 -0
  274. package/public/apple-touch-icon.png +0 -0
  275. package/public/assets/css/commands-react.input.css +71 -0
  276. package/public/assets/css/create-pack-react.input.css +31 -0
  277. package/public/assets/css/home-react.input.css +106 -0
  278. package/public/assets/css/login-react.input.css +58 -0
  279. package/public/assets/css/stickers-react.input.css +18 -0
  280. package/public/assets/css/terms-react.input.css +115 -0
  281. package/public/assets/css/user-react.input.css +57 -0
  282. package/public/assets/images/brand-icon-192.png +0 -0
  283. package/public/assets/images/brand-logo-128.webp +0 -0
  284. package/public/assets/images/hero-banner-1280.jpg +0 -0
  285. package/public/comandos/commands-catalog.json +4517 -0
  286. package/public/css/api-docs.css +161 -0
  287. package/public/css/stickers-admin.css +1288 -0
  288. package/public/css/styles.css +679 -0
  289. package/public/css/systemadm/admin.css +474 -0
  290. package/public/css/systemadm/base.css +73 -0
  291. package/public/css/systemadm/components.css +662 -0
  292. package/public/css/systemadm/layout.css +229 -0
  293. package/public/css/systemadm/tokens.css +56 -0
  294. package/public/favicon-16x16.png +0 -0
  295. package/public/favicon-32x32.png +0 -0
  296. package/public/favicon.ico +0 -0
  297. package/public/js/apps/apiDocsApp.js +235 -0
  298. package/public/js/apps/commandsReactApp.js +528 -0
  299. package/public/js/apps/createPackApp.js +1646 -0
  300. package/public/js/apps/homeReactApp.js +942 -0
  301. package/public/js/apps/loginReactApp.js +496 -0
  302. package/public/js/apps/stickersAdminApp.js +1753 -0
  303. package/public/js/apps/stickersApp.js +3797 -0
  304. package/public/js/apps/termsReactApp.js +528 -0
  305. package/public/js/apps/userApp.js +2540 -0
  306. package/public/js/apps/userProfile/actions.js +66 -0
  307. package/public/js/apps/userReactApp.js +547 -0
  308. package/public/js/catalog.js +950 -0
  309. package/public/pages/api-docs.html +40 -0
  310. package/public/pages/aup.html +158 -0
  311. package/public/pages/comandos.html +41 -0
  312. package/public/pages/dpa.html +227 -0
  313. package/public/pages/home.html +45 -0
  314. package/public/pages/licenca.html +182 -0
  315. package/public/pages/login.html +40 -0
  316. package/public/pages/notice-and-takedown.html +234 -0
  317. package/public/pages/politica-de-privacidade.html +251 -0
  318. package/public/pages/seo-bot-whatsapp-para-grupo.html +350 -0
  319. package/public/pages/seo-bot-whatsapp-sem-programar.html +350 -0
  320. package/public/pages/seo-como-automatizar-avisos-no-whatsapp.html +350 -0
  321. package/public/pages/seo-como-criar-comandos-whatsapp.html +350 -0
  322. package/public/pages/seo-como-evitar-spam-no-whatsapp.html +350 -0
  323. package/public/pages/seo-como-moderar-grupo-whatsapp.html +350 -0
  324. package/public/pages/seo-como-organizar-comunidade-whatsapp.html +350 -0
  325. package/public/pages/seo-melhor-bot-whatsapp-para-grupos.html +350 -0
  326. package/public/pages/stickers-admin.html +31 -0
  327. package/public/pages/stickers-create.html +41 -0
  328. package/public/pages/stickers.html +45 -0
  329. package/public/pages/suboperadores.html +237 -0
  330. package/public/pages/termos-de-uso-texto-integral.html +241 -0
  331. package/public/pages/termos-de-uso.html +41 -0
  332. package/public/pages/user-password-reset.html +32 -0
  333. package/public/pages/user-systemadm.html +508 -0
  334. package/public/pages/user.html +39 -0
  335. package/public/robots.txt +9 -0
  336. package/public/site.webmanifest +24 -0
  337. package/public/sitemap.xml +98 -0
  338. package/schemas/command-config.schema.json +582 -0
  339. package/scripts/baileys-compat-smoke.mjs +12 -0
  340. package/scripts/cache-bust.mjs +142 -0
  341. package/scripts/deploy.sh +916 -0
  342. package/scripts/email-broadcast-terms-update.mjs +170 -0
  343. package/scripts/enrich-command-discovery-fields.mjs +286 -0
  344. package/scripts/generate-command-config-schema.mjs +273 -0
  345. package/scripts/generate-commands-catalog.mjs +308 -0
  346. package/scripts/generate-module-agents.mjs +631 -0
  347. package/scripts/generate-seo-satellite-pages.mjs +400 -0
  348. package/scripts/github-deploy-notify.mjs +174 -0
  349. package/scripts/github-release-notify.mjs +219 -0
  350. package/scripts/release.sh +599 -0
  351. package/scripts/run-codeql-local.sh +116 -0
  352. package/scripts/run-prettier-all.mjs +25 -0
  353. package/scripts/security-smoketest.mjs +581 -0
  354. package/scripts/sticker-catalog-loadtest.mjs +210 -0
  355. package/scripts/sticker-worker-task.mjs +119 -0
  356. package/scripts/sync-readme-snapshot.mjs +133 -0
  357. package/scripts/validate-command-config-schema.mjs +130 -0
  358. package/scripts/validate-command-configs.mjs +15 -0
  359. package/scripts/wiki-sync.sh +191 -0
  360. package/server/auth/googleWebAuth/googleWebAuthRuntime.js +62 -0
  361. package/server/auth/googleWebAuth/googleWebAuthService.js +807 -0
  362. package/server/auth/jwt/webJwtService.js +147 -0
  363. package/server/auth/stickerCatalogAuthContext.js +165 -0
  364. package/server/auth/termsAcceptance/termsAcceptanceHandler.js +189 -0
  365. package/server/auth/userPassword/index.js +14 -0
  366. package/server/auth/userPassword/userPasswordAuthService.js +422 -0
  367. package/server/auth/userPassword/userPasswordCrypto.js +199 -0
  368. package/server/auth/userPassword/userPasswordCrypto.test.js +76 -0
  369. package/server/auth/userPassword/userPasswordRecoveryService.js +728 -0
  370. package/server/auth/validation/authSchemas.js +236 -0
  371. package/server/auth/webAccount/webAccountHandlers.js +1434 -0
  372. package/server/controllers/admin/adminBanService.js +138 -0
  373. package/server/controllers/admin/adminPanelHandlers.js +2083 -0
  374. package/server/controllers/admin/stickerCatalogAdminContext.js +17 -0
  375. package/server/controllers/admin/systemAdminController.js +201 -0
  376. package/server/controllers/email/emailAutomationController.js +239 -0
  377. package/server/controllers/metricsController.js +21 -0
  378. package/server/controllers/seo/stickerCatalogSeoContext.js +514 -0
  379. package/server/controllers/sticker/nonCatalogHandlers.js +303 -0
  380. package/server/controllers/sticker/stickerCatalogController.js +4700 -0
  381. package/server/controllers/system/contactController.js +115 -0
  382. package/server/controllers/system/githubController.js +137 -0
  383. package/server/controllers/system/stickerCatalogSystemContext.js +758 -0
  384. package/server/controllers/system/storageController.js +154 -0
  385. package/server/controllers/system/systemController.js +135 -0
  386. package/server/controllers/system/systemMetricsController.js +156 -0
  387. package/server/controllers/system/visitController.js +90 -0
  388. package/server/controllers/userController.js +145 -0
  389. package/server/email/emailAutomationRuntime.js +225 -0
  390. package/server/email/emailAutomationService.js +125 -0
  391. package/server/email/emailOutboxRepository.js +282 -0
  392. package/server/email/emailTemplateService.js +480 -0
  393. package/server/email/emailTransportService.js +156 -0
  394. package/server/http/clientIp.js +95 -0
  395. package/server/http/httpRequestUtils.js +262 -0
  396. package/server/http/httpRequestUtils.test.js +80 -0
  397. package/server/http/httpServer.js +180 -0
  398. package/server/http/requestContext.js +20 -0
  399. package/server/http/siteRoutingUtils.js +87 -0
  400. package/server/index.js +1 -0
  401. package/server/middleware/cachePolicy.js +26 -0
  402. package/server/middleware/cachePolicyHelpers.js +1 -0
  403. package/server/middleware/endpointRateLimit.js +181 -0
  404. package/server/middleware/rateLimit.js +70 -0
  405. package/server/middleware/requireAdminAuth.js +48 -0
  406. package/server/middleware/securityHeaders.js +97 -0
  407. package/server/routes/admin/systemAdminRouter.js +64 -0
  408. package/server/routes/email/emailAutomationRouter.js +46 -0
  409. package/server/routes/health/healthRouter.js +41 -0
  410. package/server/routes/indexRouter.js +234 -0
  411. package/server/routes/metrics/metricsRouter.js +58 -0
  412. package/server/routes/static/staticPageRouter.js +134 -0
  413. package/server/routes/sticker/catalogHandlers/catalogAdminHttp.js +105 -0
  414. package/server/routes/sticker/catalogHandlers/catalogAuthHttp.js +77 -0
  415. package/server/routes/sticker/catalogHandlers/catalogPublicHttp.js +120 -0
  416. package/server/routes/sticker/catalogHandlers/catalogUploadHttp.js +83 -0
  417. package/server/routes/sticker/catalogRouter.js +77 -0
  418. package/server/routes/sticker/stickerApiRouter.js +84 -0
  419. package/server/routes/sticker/stickerDataRouter.js +145 -0
  420. package/server/routes/sticker/stickerSiteRouter.js +43 -0
  421. package/server/routes/user/userApiPaths.js +66 -0
  422. package/server/routes/user/userRouter.js +65 -0
  423. package/server/utils/safePath.js +26 -0
  424. package/utils/logger/loggerModule.js +35 -0
  425. package/vite.config.mjs +38 -0
@@ -0,0 +1,80 @@
1
+ import { executeQuery, TABLES } from '../../../database/index.js';
2
+ import { normalizeJid } from '../../config/index.js';
3
+ import { toWhatsAppPhoneDigits } from './whatsappLoginLinkService.js';
4
+
5
+ const parseEnvInt = (value, fallback, min, max) => {
6
+ const numeric = Number(value);
7
+ if (!Number.isFinite(numeric)) return fallback;
8
+ return Math.max(min, Math.min(max, Math.floor(numeric)));
9
+ };
10
+
11
+ const GOOGLE_LINK_CHECK_CACHE_TTL_MS = parseEnvInt(process.env.WHATSAPP_GOOGLE_LINK_CHECK_CACHE_TTL_MS, 60_000, 1_000, 10 * 60_000);
12
+ const googleLinkCheckCache = new Map();
13
+ let googleLinkTableMissingLogged = false;
14
+
15
+ const normalizeCacheKey = ({ ownerJid = '', ownerPhone = '' }) => {
16
+ const normalizedOwnerJid = normalizeJid(ownerJid) || '';
17
+ const normalizedOwnerPhone = toWhatsAppPhoneDigits(ownerPhone || ownerJid) || '';
18
+ return `${normalizedOwnerJid}|${normalizedOwnerPhone}`;
19
+ };
20
+
21
+ const getCachedGoogleLinkStatus = (cacheKey) => {
22
+ const cached = googleLinkCheckCache.get(cacheKey);
23
+ if (!cached) return null;
24
+ if (Number(cached.expiresAt || 0) <= Date.now()) {
25
+ googleLinkCheckCache.delete(cacheKey);
26
+ return null;
27
+ }
28
+ return Boolean(cached.linked);
29
+ };
30
+
31
+ const setCachedGoogleLinkStatus = (cacheKey, linked) => {
32
+ googleLinkCheckCache.set(cacheKey, {
33
+ linked: Boolean(linked),
34
+ expiresAt: Date.now() + GOOGLE_LINK_CHECK_CACHE_TTL_MS,
35
+ });
36
+ };
37
+
38
+ export const isWhatsAppUserLinkedToGoogleWebAccount = async ({ ownerJid = '', ownerPhone = '' } = {}) => {
39
+ const normalizedOwnerJid = normalizeJid(ownerJid) || '';
40
+ const normalizedOwnerPhone = toWhatsAppPhoneDigits(ownerPhone || ownerJid) || '';
41
+ if (!normalizedOwnerJid && !normalizedOwnerPhone) return false;
42
+
43
+ const cacheKey = normalizeCacheKey({
44
+ ownerJid: normalizedOwnerJid,
45
+ ownerPhone: normalizedOwnerPhone,
46
+ });
47
+ const cached = getCachedGoogleLinkStatus(cacheKey);
48
+ if (cached !== null) return cached;
49
+
50
+ const whereClauses = [];
51
+ const params = [];
52
+ if (normalizedOwnerJid) {
53
+ whereClauses.push('owner_jid = ?');
54
+ params.push(normalizedOwnerJid);
55
+ }
56
+ if (normalizedOwnerPhone) {
57
+ whereClauses.push('owner_phone = ?');
58
+ params.push(normalizedOwnerPhone);
59
+ }
60
+
61
+ const rows = await executeQuery(
62
+ `SELECT google_sub
63
+ FROM ${TABLES.STICKER_WEB_GOOGLE_USER}
64
+ WHERE ${whereClauses.join(' OR ')}
65
+ LIMIT 1`,
66
+ params,
67
+ ).catch((error) => {
68
+ if (error?.code === 'ER_NO_SUCH_TABLE') {
69
+ if (!googleLinkTableMissingLogged) {
70
+ googleLinkTableMissingLogged = true;
71
+ }
72
+ return [];
73
+ }
74
+ throw error;
75
+ });
76
+
77
+ const linked = Array.isArray(rows) && rows.length > 0;
78
+ setCachedGoogleLinkStatus(cacheKey, linked);
79
+ return linked;
80
+ };
@@ -0,0 +1,230 @@
1
+ import { createHmac, timingSafeEqual } from 'node:crypto';
2
+ import { URL } from 'node:url';
3
+ import { getJidServer, getJidUser, normalizeJid } from '../../config/index.js';
4
+
5
+ const WHATSAPP_USER_SERVERS = new Set(['s.whatsapp.net', 'c.us', 'hosted']);
6
+ const DEFAULT_LOGIN_BASE_URL = 'https://omnizap.shop';
7
+ const SIGNING_SECRET = String(process.env.WHATSAPP_LOGIN_LINK_SECRET || '').trim();
8
+ const SIGNED_LINKS_ENABLED = Boolean(SIGNING_SECRET);
9
+ const REQUIRE_SIGNATURE = parseEnvBool(process.env.WHATSAPP_LOGIN_REQUIRE_SIGNATURE, SIGNED_LINKS_ENABLED);
10
+ const LOGIN_TTL_SECONDS = Math.max(60, Number(process.env.WHATSAPP_LOGIN_LINK_TTL_SECONDS) || 15 * 60);
11
+ const LOGIN_PATH = normalizeLoginPath(process.env.WHATSAPP_LOGIN_PATH || '/login/');
12
+
13
+ function parseEnvBool(value, fallback) {
14
+ if (value === undefined || value === null || value === '') return fallback;
15
+ const normalized = String(value).trim().toLowerCase();
16
+ if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
17
+ if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
18
+ return fallback;
19
+ }
20
+
21
+ function normalizePhoneDigits(value) {
22
+ return String(value || '').replace(/\D+/g, '');
23
+ }
24
+
25
+ function normalizeLoginPath(value) {
26
+ const raw = String(value || '').trim();
27
+ if (!raw) return '/login/';
28
+ const withLeadingSlash = raw.startsWith('/') ? raw : `/${raw}`;
29
+ return withLeadingSlash.endsWith('/') ? withLeadingSlash : `${withLeadingSlash}/`;
30
+ }
31
+
32
+ function sanitizeTimestamp(value) {
33
+ const parsed = Number(value);
34
+ if (!Number.isFinite(parsed) || parsed <= 0) return 0;
35
+ return Math.floor(parsed);
36
+ }
37
+
38
+ function buildSignaturePayload(phoneDigits, tsSeconds) {
39
+ return `${phoneDigits}.${tsSeconds}`;
40
+ }
41
+
42
+ function buildHintSignature(phoneDigits, tsSeconds) {
43
+ if (!SIGNED_LINKS_ENABLED) return '';
44
+ return createHmac('sha256', SIGNING_SECRET).update(buildSignaturePayload(phoneDigits, tsSeconds)).digest('hex');
45
+ }
46
+
47
+ function safeHexCompare(left, right) {
48
+ const leftHex = String(left || '')
49
+ .trim()
50
+ .toLowerCase();
51
+ const rightHex = String(right || '')
52
+ .trim()
53
+ .toLowerCase();
54
+ if (!leftHex || !rightHex || leftHex.length !== rightHex.length) return false;
55
+
56
+ try {
57
+ const leftBuffer = Buffer.from(leftHex, 'hex');
58
+ const rightBuffer = Buffer.from(rightHex, 'hex');
59
+ if (!leftBuffer.length || leftBuffer.length !== rightBuffer.length) return false;
60
+ return timingSafeEqual(leftBuffer, rightBuffer);
61
+ } catch {
62
+ return false;
63
+ }
64
+ }
65
+
66
+ function resolveLoginBaseUrl(explicitBaseUrl = '') {
67
+ const candidates = [explicitBaseUrl, process.env.WHATSAPP_LOGIN_BASE_URL, process.env.SITE_ORIGIN, process.env.PUBLIC_WEB_BASE_URL, DEFAULT_LOGIN_BASE_URL];
68
+
69
+ for (const candidate of candidates) {
70
+ const raw = String(candidate || '').trim();
71
+ if (!raw) continue;
72
+ try {
73
+ const url = new URL(raw);
74
+ return `${url.origin}`;
75
+ } catch (error) {
76
+ void error;
77
+ }
78
+ }
79
+
80
+ return DEFAULT_LOGIN_BASE_URL;
81
+ }
82
+
83
+ export const toWhatsAppPhoneDigits = (value) => {
84
+ const raw = String(value || '').trim();
85
+ if (!raw) return '';
86
+
87
+ if (raw.includes('@')) {
88
+ const normalizedJid = normalizeJid(raw);
89
+ const server = getJidServer(normalizedJid);
90
+ if (!WHATSAPP_USER_SERVERS.has(server)) return '';
91
+ const jidUser = String(getJidUser(normalizedJid) || '').split(':')[0];
92
+ const digits = normalizePhoneDigits(jidUser);
93
+ return digits.length >= 10 && digits.length <= 15 ? digits : '';
94
+ }
95
+
96
+ const digits = normalizePhoneDigits(raw);
97
+ return digits.length >= 10 && digits.length <= 15 ? digits : '';
98
+ };
99
+
100
+ export const toWhatsAppOwnerJid = (value) => {
101
+ const digits = toWhatsAppPhoneDigits(value);
102
+ if (!digits) return '';
103
+ return normalizeJid(`${digits}@s.whatsapp.net`) || '';
104
+ };
105
+
106
+ export const buildWhatsAppLoginHint = (value, { nowMs = Date.now() } = {}) => {
107
+ const phoneDigits = toWhatsAppPhoneDigits(value);
108
+ if (!phoneDigits) return null;
109
+
110
+ const tsSeconds = Math.floor(nowMs / 1000);
111
+ const hint = {
112
+ wa: phoneDigits,
113
+ wa_ts: String(tsSeconds),
114
+ };
115
+
116
+ const signature = buildHintSignature(phoneDigits, tsSeconds);
117
+ if (signature) hint.wa_sig = signature;
118
+
119
+ return hint;
120
+ };
121
+
122
+ export const buildWhatsAppGoogleLoginUrl = ({ userId, baseUrl } = {}) => {
123
+ const hint = buildWhatsAppLoginHint(userId);
124
+ if (!hint) return '';
125
+
126
+ const root = resolveLoginBaseUrl(baseUrl);
127
+ const url = new URL(LOGIN_PATH, root);
128
+ url.searchParams.set('wa', hint.wa);
129
+ url.searchParams.set('wa_ts', hint.wa_ts);
130
+ if (hint.wa_sig) url.searchParams.set('wa_sig', hint.wa_sig);
131
+ return url.toString();
132
+ };
133
+
134
+ export const extractWhatsAppLoginHint = (payload = {}) => {
135
+ const source = payload && typeof payload === 'object' ? payload : {};
136
+ const nested = source.whatsapp_login && typeof source.whatsapp_login === 'object' ? source.whatsapp_login : {};
137
+ return {
138
+ wa: String(source.wa ?? source.whatsapp_phone ?? source.owner_phone ?? nested.wa ?? nested.phone ?? '').trim(),
139
+ wa_ts: String(source.wa_ts ?? source.whatsapp_ts ?? source.owner_phone_ts ?? nested.wa_ts ?? nested.ts ?? '').trim(),
140
+ wa_sig: String(source.wa_sig ?? source.whatsapp_sig ?? source.owner_phone_sig ?? nested.wa_sig ?? nested.sig ?? '').trim(),
141
+ };
142
+ };
143
+
144
+ export const resolveWhatsAppOwnerJidFromLoginPayload = (payload, { nowMs = Date.now() } = {}) => {
145
+ const hint = extractWhatsAppLoginHint(payload);
146
+ const hasPayload = Boolean(hint.wa || hint.wa_ts || hint.wa_sig);
147
+ if (!hasPayload) {
148
+ return {
149
+ hasPayload: false,
150
+ ownerJid: '',
151
+ verified: false,
152
+ signed: false,
153
+ reason: '',
154
+ };
155
+ }
156
+
157
+ const phoneDigits = toWhatsAppPhoneDigits(hint.wa);
158
+ const ownerJid = toWhatsAppOwnerJid(phoneDigits);
159
+ if (!ownerJid) {
160
+ return {
161
+ hasPayload: true,
162
+ ownerJid: '',
163
+ verified: false,
164
+ signed: false,
165
+ reason: 'invalid_phone',
166
+ };
167
+ }
168
+
169
+ if (!SIGNED_LINKS_ENABLED) {
170
+ return {
171
+ hasPayload: true,
172
+ ownerJid,
173
+ verified: false,
174
+ signed: false,
175
+ reason: '',
176
+ };
177
+ }
178
+
179
+ const tsSeconds = sanitizeTimestamp(hint.wa_ts);
180
+ const hasSignature = Boolean(hint.wa_sig);
181
+
182
+ if (!tsSeconds || !hasSignature) {
183
+ if (REQUIRE_SIGNATURE) {
184
+ return {
185
+ hasPayload: true,
186
+ ownerJid: '',
187
+ verified: false,
188
+ signed: false,
189
+ reason: 'missing_signature',
190
+ };
191
+ }
192
+ return {
193
+ hasPayload: true,
194
+ ownerJid,
195
+ verified: false,
196
+ signed: false,
197
+ reason: '',
198
+ };
199
+ }
200
+
201
+ const nowSeconds = Math.floor(nowMs / 1000);
202
+ if (Math.abs(nowSeconds - tsSeconds) > LOGIN_TTL_SECONDS) {
203
+ return {
204
+ hasPayload: true,
205
+ ownerJid: '',
206
+ verified: false,
207
+ signed: true,
208
+ reason: 'expired',
209
+ };
210
+ }
211
+
212
+ const expectedSignature = buildHintSignature(phoneDigits, tsSeconds);
213
+ if (!safeHexCompare(expectedSignature, hint.wa_sig)) {
214
+ return {
215
+ hasPayload: true,
216
+ ownerJid: '',
217
+ verified: false,
218
+ signed: true,
219
+ reason: 'invalid_signature',
220
+ };
221
+ }
222
+
223
+ return {
224
+ hasPayload: true,
225
+ ownerJid,
226
+ verified: true,
227
+ signed: true,
228
+ reason: '',
229
+ };
230
+ };
@@ -0,0 +1,398 @@
1
+ import axios from 'axios';
2
+ import logger from '#logger';
3
+ import { recordPokeApiCacheHit } from '../../observability/metrics.js';
4
+
5
+ const BASE_URL = 'https://pokeapi.co/api/v2';
6
+ const MIN_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
7
+ const CACHE_TTL_MS = Math.max(MIN_CACHE_TTL_MS, Number(process.env.POKEAPI_CACHE_TTL_MS) || MIN_CACHE_TTL_MS);
8
+ const REQUEST_TIMEOUT_MS = Math.max(3_000, Number(process.env.POKEAPI_TIMEOUT_MS) || 10_000);
9
+ const REQUEST_RETRY_ATTEMPTS = Math.max(0, Number(process.env.POKEAPI_RETRY_ATTEMPTS) || 2);
10
+ const REQUEST_RETRY_BASE_DELAY_MS = Math.max(120, Number(process.env.POKEAPI_RETRY_BASE_DELAY_MS) || 350);
11
+ const DNS_FAMILY = [0, 4, 6].includes(Number(process.env.POKEAPI_DNS_FAMILY)) ? Number(process.env.POKEAPI_DNS_FAMILY) : 4;
12
+ const REQUEST_USER_AGENT = String(process.env.POKEAPI_USER_AGENT || 'omnizap-system/2.1 (+https://github.com/Omnizap-System/omnizap)').trim();
13
+ const DEFAULT_LORE_LANGUAGES = String(process.env.POKEAPI_LORE_LANGS || 'pt-br,pt,en')
14
+ .split(',')
15
+ .map((entry) =>
16
+ String(entry || '')
17
+ .trim()
18
+ .toLowerCase(),
19
+ )
20
+ .filter(Boolean);
21
+
22
+ const sharedCache = globalThis.__omnizapPokeApiCache instanceof Map ? globalThis.__omnizapPokeApiCache : new Map();
23
+ const sharedInflight = globalThis.__omnizapPokeApiInflight instanceof Map ? globalThis.__omnizapPokeApiInflight : new Map();
24
+
25
+ globalThis.__omnizapPokeApiCache = sharedCache;
26
+ globalThis.__omnizapPokeApiInflight = sharedInflight;
27
+
28
+ const normalizeKeyPart = (value) => {
29
+ if (value === null || value === undefined) {
30
+ throw new Error('Identificador inválido para recurso da PokéAPI.');
31
+ }
32
+
33
+ const raw = String(value).trim();
34
+ if (!raw) {
35
+ throw new Error('Identificador vazio para recurso da PokéAPI.');
36
+ }
37
+
38
+ return raw.toLowerCase();
39
+ };
40
+
41
+ export const normalizeApiText = (value) => {
42
+ return String(value || '')
43
+ .replace(/[\n\r\f\t]+/g, ' ')
44
+ .replace(/\s+/g, ' ')
45
+ .trim();
46
+ };
47
+
48
+ const resolveEntryLang = (entry) =>
49
+ String(entry?.language?.name || '')
50
+ .trim()
51
+ .toLowerCase();
52
+
53
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
54
+
55
+ const hasErrorMessage = (value) => typeof value === 'string' && value.trim().length > 0;
56
+
57
+ const extractCauseMessages = (error) => {
58
+ const causes = Array.isArray(error?.cause?.errors) ? error.cause.errors : Array.isArray(error?.errors) ? error.errors : [];
59
+
60
+ const messages = causes.map((entry) => String(entry?.message || '').trim()).filter(Boolean);
61
+ return messages;
62
+ };
63
+
64
+ const summarizeRequestError = (error) => {
65
+ const directMessage = String(error?.message || '').trim();
66
+ const causeMessage = String(error?.cause?.message || '').trim();
67
+ const status = Number(error?.response?.status);
68
+ const code = String(error?.code || error?.cause?.code || '').trim() || null;
69
+ const causeMessages = extractCauseMessages(error);
70
+
71
+ const message = directMessage || causeMessage || causeMessages[0] || (Number.isFinite(status) ? `HTTP ${status}` : null) || (code ? `Erro de rede (${code})` : null) || 'erro-desconhecido';
72
+
73
+ return {
74
+ message,
75
+ code,
76
+ status: Number.isFinite(status) ? status : null,
77
+ causeMessages: causeMessages.slice(0, 3),
78
+ };
79
+ };
80
+
81
+ const isRetryableRequestError = (error) => {
82
+ const status = Number(error?.response?.status);
83
+ if (Number.isFinite(status)) {
84
+ return status === 408 || status === 425 || status === 429 || (status >= 500 && status < 600);
85
+ }
86
+
87
+ const code = String(error?.code || error?.cause?.code || '')
88
+ .trim()
89
+ .toUpperCase();
90
+ if (!code) return false;
91
+ return ['ETIMEDOUT', 'ECONNRESET', 'ECONNABORTED', 'EAI_AGAIN', 'ENETUNREACH', 'EHOSTUNREACH', 'EPIPE'].includes(code);
92
+ };
93
+
94
+ const calculateRetryDelay = (attempt) => {
95
+ const exponential = REQUEST_RETRY_BASE_DELAY_MS * 2 ** Math.max(0, attempt);
96
+ return Math.min(2_500, exponential + Math.floor(Math.random() * 180));
97
+ };
98
+
99
+ const pickEntryByLangPriority = (entries = [], languages = DEFAULT_LORE_LANGUAGES) => {
100
+ const list = Array.isArray(entries) ? entries : [];
101
+ const langPriority = (Array.isArray(languages) ? languages : DEFAULT_LORE_LANGUAGES)
102
+ .map((entry) =>
103
+ String(entry || '')
104
+ .trim()
105
+ .toLowerCase(),
106
+ )
107
+ .filter(Boolean);
108
+ if (!list.length) return null;
109
+ if (!langPriority.length) return list[0] || null;
110
+
111
+ for (const lang of langPriority) {
112
+ const found = list.find((entry) => resolveEntryLang(entry) === lang);
113
+ if (found) return found;
114
+ }
115
+
116
+ const english = list.find((entry) => resolveEntryLang(entry) === 'en');
117
+ if (english) return english;
118
+ return list[0] || null;
119
+ };
120
+
121
+ export const getLocalizedName = (names = [], fallback = null, languages = DEFAULT_LORE_LANGUAGES) => {
122
+ const entry = pickEntryByLangPriority(names, languages);
123
+ const value = normalizeApiText(entry?.name || '');
124
+ return value || normalizeApiText(fallback || '') || null;
125
+ };
126
+
127
+ export const getLocalizedGenus = (genera = [], fallback = null, languages = DEFAULT_LORE_LANGUAGES) => {
128
+ const entry = pickEntryByLangPriority(genera, languages);
129
+ const value = normalizeApiText(entry?.genus || '');
130
+ return value || normalizeApiText(fallback || '') || null;
131
+ };
132
+
133
+ export const getFlavorText = (flavorTextEntries = [], options = {}) => {
134
+ const entries = Array.isArray(flavorTextEntries) ? flavorTextEntries : [];
135
+ if (!entries.length) return null;
136
+
137
+ const languages = Array.isArray(options?.languages) ? options.languages : DEFAULT_LORE_LANGUAGES;
138
+ const preferredVersion = String(options?.version || '')
139
+ .trim()
140
+ .toLowerCase();
141
+ const filtered = preferredVersion
142
+ ? entries.filter(
143
+ (entry) =>
144
+ String(entry?.version?.name || '')
145
+ .trim()
146
+ .toLowerCase() === preferredVersion,
147
+ )
148
+ : entries;
149
+
150
+ const entry = pickEntryByLangPriority(filtered.length ? filtered : entries, languages);
151
+ const value = normalizeApiText(entry?.flavor_text || entry?.text || '');
152
+ return value || null;
153
+ };
154
+
155
+ export const getEffectText = (effectEntries = [], options = {}) => {
156
+ const entries = Array.isArray(effectEntries) ? effectEntries : [];
157
+ if (!entries.length) return null;
158
+ const languages = Array.isArray(options?.languages) ? options.languages : DEFAULT_LORE_LANGUAGES;
159
+ const entry = pickEntryByLangPriority(entries, languages);
160
+ const shortFirst = options?.preferLong ? false : true;
161
+ const primary = shortFirst ? entry?.short_effect : entry?.effect;
162
+ const fallback = shortFirst ? entry?.effect : entry?.short_effect;
163
+ const value = normalizeApiText(primary || fallback || '');
164
+ return value || null;
165
+ };
166
+
167
+ export const getDefaultLoreLanguages = () => [...DEFAULT_LORE_LANGUAGES];
168
+
169
+ const cleanupExpiredEntry = (key, now) => {
170
+ const entry = sharedCache.get(key);
171
+ if (!entry) return null;
172
+ if (entry.expiresAt <= now) {
173
+ sharedCache.delete(key);
174
+ return null;
175
+ }
176
+ return entry.data;
177
+ };
178
+
179
+ const requestResource = async ({ path, cacheKey }) => {
180
+ const now = Date.now();
181
+ const staleEntry = sharedCache.get(cacheKey);
182
+ const staleData = staleEntry?.data || null;
183
+ const cached = cleanupExpiredEntry(cacheKey, now);
184
+ if (cached) {
185
+ recordPokeApiCacheHit();
186
+ return cached;
187
+ }
188
+
189
+ if (sharedInflight.has(cacheKey)) {
190
+ return sharedInflight.get(cacheKey);
191
+ }
192
+
193
+ const requestPromise = (async () => {
194
+ let lastError = null;
195
+ const maxAttempts = Math.max(1, REQUEST_RETRY_ATTEMPTS + 1);
196
+
197
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
198
+ try {
199
+ const response = await axios.get(`${BASE_URL}${path}`, {
200
+ timeout: REQUEST_TIMEOUT_MS,
201
+ family: DNS_FAMILY,
202
+ headers: {
203
+ Accept: 'application/json',
204
+ 'User-Agent': REQUEST_USER_AGENT,
205
+ },
206
+ });
207
+ const data = response?.data;
208
+ sharedCache.set(cacheKey, {
209
+ data,
210
+ expiresAt: Date.now() + CACHE_TTL_MS,
211
+ });
212
+ return data;
213
+ } catch (error) {
214
+ lastError = error;
215
+ const shouldRetry = attempt < maxAttempts - 1 && isRetryableRequestError(error);
216
+ if (shouldRetry) {
217
+ await sleep(calculateRetryDelay(attempt));
218
+ continue;
219
+ }
220
+ break;
221
+ }
222
+ }
223
+
224
+ const details = summarizeRequestError(lastError);
225
+ if (staleData) {
226
+ logger.warn('Falha ao consultar PokéAPI. Usando cache expirado.', {
227
+ path,
228
+ error: details.message,
229
+ code: details.code,
230
+ status: details.status,
231
+ attempts: maxAttempts,
232
+ causeMessages: details.causeMessages,
233
+ });
234
+ return staleData;
235
+ }
236
+
237
+ logger.warn('Falha ao consultar PokéAPI.', {
238
+ path,
239
+ error: details.message,
240
+ code: details.code,
241
+ status: details.status,
242
+ attempts: maxAttempts,
243
+ causeMessages: details.causeMessages,
244
+ });
245
+
246
+ if (!lastError || typeof lastError !== 'object') {
247
+ const normalizedError = new Error(details.message);
248
+ normalizedError.code = details.code;
249
+ normalizedError.status = details.status;
250
+ normalizedError.pokeApiMeta = {
251
+ code: details.code,
252
+ status: details.status,
253
+ attempts: maxAttempts,
254
+ causeMessages: details.causeMessages,
255
+ };
256
+ throw normalizedError;
257
+ }
258
+
259
+ if (!hasErrorMessage(lastError.message)) {
260
+ lastError.message = details.message;
261
+ }
262
+ lastError.pokeApiMeta = {
263
+ code: details.code,
264
+ status: details.status,
265
+ attempts: maxAttempts,
266
+ causeMessages: details.causeMessages,
267
+ };
268
+ throw lastError;
269
+ })().finally(() => {
270
+ sharedInflight.delete(cacheKey);
271
+ });
272
+
273
+ sharedInflight.set(cacheKey, requestPromise);
274
+ return requestPromise;
275
+ };
276
+
277
+ const getNamedResource = async (resource, idOrName) => {
278
+ const normalized = normalizeKeyPart(idOrName);
279
+ return requestResource({
280
+ path: `/${resource}/${encodeURIComponent(normalized)}`,
281
+ cacheKey: `${resource}:${normalized}`,
282
+ });
283
+ };
284
+
285
+ export const getResourceList = async ({ resource, limit = 20, offset = 0 }) => {
286
+ const safeLimit = Math.max(1, Math.min(200, Number(limit) || 20));
287
+ const safeOffset = Math.max(0, Number(offset) || 0);
288
+ const cacheKey = `list:${resource}:${safeLimit}:${safeOffset}`;
289
+ const query = `?limit=${encodeURIComponent(String(safeLimit))}&offset=${encodeURIComponent(String(safeOffset))}`;
290
+ return requestResource({
291
+ path: `/${resource}${query}`,
292
+ cacheKey,
293
+ });
294
+ };
295
+
296
+ export const getPokemonImage = (pokemonApiResponse, options = {}) => {
297
+ const shinyPreferred = Boolean(options?.shiny);
298
+ if (shinyPreferred) {
299
+ const shinyFront = pokemonApiResponse?.sprites?.front_shiny;
300
+ if (typeof shinyFront === 'string' && shinyFront.trim()) {
301
+ return shinyFront.trim();
302
+ }
303
+ }
304
+
305
+ const officialArtwork = pokemonApiResponse?.sprites?.other?.['official-artwork']?.front_default;
306
+ if (typeof officialArtwork === 'string' && officialArtwork.trim()) {
307
+ return officialArtwork.trim();
308
+ }
309
+
310
+ const frontDefault = pokemonApiResponse?.sprites?.front_default;
311
+ if (typeof frontDefault === 'string' && frontDefault.trim()) {
312
+ return frontDefault.trim();
313
+ }
314
+
315
+ return null;
316
+ };
317
+
318
+ export const getPokemon = async (idOrName) => {
319
+ return getNamedResource('pokemon', idOrName);
320
+ };
321
+
322
+ export const getMove = async (idOrName) => {
323
+ return getNamedResource('move', idOrName);
324
+ };
325
+
326
+ export const getType = async (name) => {
327
+ return getNamedResource('type', name);
328
+ };
329
+
330
+ export const getSpecies = async (id) => {
331
+ const normalized = normalizeKeyPart(id);
332
+ return requestResource({
333
+ path: `/pokemon-species/${encodeURIComponent(normalized)}`,
334
+ cacheKey: `species:${normalized}`,
335
+ });
336
+ };
337
+
338
+ export const getEvolutionChain = async (id) => {
339
+ return getNamedResource('evolution-chain', id);
340
+ };
341
+
342
+ export const getItem = async (idOrName) => getNamedResource('item', idOrName);
343
+
344
+ export const getItemCategory = async (idOrName) => getNamedResource('item-category', idOrName);
345
+
346
+ export const getItemPocket = async (idOrName) => getNamedResource('item-pocket', idOrName);
347
+
348
+ export const getMachine = async (idOrName) => getNamedResource('machine', idOrName);
349
+
350
+ export const getBerry = async (idOrName) => getNamedResource('berry', idOrName);
351
+
352
+ export const getBerryFlavor = async (idOrName) => getNamedResource('berry-flavor', idOrName);
353
+
354
+ export const getRegion = async (idOrName) => getNamedResource('region', idOrName);
355
+
356
+ export const getLocation = async (idOrName) => getNamedResource('location', idOrName);
357
+
358
+ export const getLocationArea = async (idOrName) => getNamedResource('location-area', idOrName);
359
+
360
+ export const getPokedex = async (idOrName) => getNamedResource('pokedex', idOrName);
361
+
362
+ export const getGeneration = async (idOrName) => getNamedResource('generation', idOrName);
363
+
364
+ export const getNature = async (idOrName) => getNamedResource('nature', idOrName);
365
+
366
+ export const getAbility = async (idOrName) => getNamedResource('ability', idOrName);
367
+
368
+ export const getCharacteristic = async (idOrName) => getNamedResource('characteristic', idOrName);
369
+
370
+ export default {
371
+ getPokemon,
372
+ getMove,
373
+ getType,
374
+ getSpecies,
375
+ getEvolutionChain,
376
+ getItem,
377
+ getItemCategory,
378
+ getItemPocket,
379
+ getMachine,
380
+ getBerry,
381
+ getBerryFlavor,
382
+ getRegion,
383
+ getLocation,
384
+ getLocationArea,
385
+ getPokedex,
386
+ getGeneration,
387
+ getNature,
388
+ getAbility,
389
+ getCharacteristic,
390
+ getLocalizedName,
391
+ getLocalizedGenus,
392
+ getFlavorText,
393
+ getEffectText,
394
+ getDefaultLoreLanguages,
395
+ normalizeApiText,
396
+ getResourceList,
397
+ getPokemonImage,
398
+ };