@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,422 @@
1
+ import { hashUserPassword, resolveUserPasswordPolicy, validateUserPassword, verifyUserPasswordHash } from './userPasswordCrypto.js';
2
+
3
+ const clampInt = (value, fallback, min, max) => {
4
+ const numeric = Number(value);
5
+ if (!Number.isFinite(numeric)) return fallback;
6
+ return Math.max(min, Math.min(max, Math.floor(numeric)));
7
+ };
8
+
9
+ const normalizeGoogleSubject = (value) =>
10
+ String(value || '')
11
+ .trim()
12
+ .replace(/[^a-zA-Z0-9_-]/g, '')
13
+ .slice(0, 80);
14
+
15
+ const normalizeEmail = (value) =>
16
+ String(value || '')
17
+ .trim()
18
+ .toLowerCase()
19
+ .slice(0, 255);
20
+
21
+ const normalizeJid = (value) =>
22
+ String(value || '')
23
+ .trim()
24
+ .toLowerCase()
25
+ .slice(0, 255);
26
+
27
+ const normalizeName = (value) =>
28
+ String(value || '')
29
+ .trim()
30
+ .slice(0, 120) || null;
31
+
32
+ const toIsoOrNull = (value) => {
33
+ const timestamp = Number(new Date(value || 0));
34
+ if (!Number.isFinite(timestamp) || timestamp <= 0) return null;
35
+ return new Date(timestamp).toISOString();
36
+ };
37
+
38
+ const buildHttpError = (message, { statusCode = 400, code = 'BAD_REQUEST' } = {}) => {
39
+ const error = new Error(String(message || 'Erro interno.'));
40
+ error.statusCode = Number(statusCode) || 400;
41
+ error.code = String(code || 'BAD_REQUEST');
42
+ return error;
43
+ };
44
+
45
+ const normalizeIdentity = ({ googleSub = '', email = '', ownerJid = '' } = {}) => {
46
+ const normalizedGoogleSub = normalizeGoogleSubject(googleSub);
47
+ const normalizedEmail = normalizeEmail(email);
48
+ const normalizedOwnerJid = normalizeJid(ownerJid);
49
+ return {
50
+ googleSub: normalizedGoogleSub,
51
+ email: normalizedEmail,
52
+ ownerJid: normalizedOwnerJid,
53
+ hasIdentity: Boolean(normalizedGoogleSub || normalizedEmail || normalizedOwnerJid),
54
+ };
55
+ };
56
+
57
+ const buildIdentityFilterClause = ({ googleSub = '', email = '', ownerJid = '' } = {}, tableAlias = 'u') => {
58
+ const clauses = [];
59
+ const params = [];
60
+
61
+ if (googleSub) {
62
+ clauses.push(`${tableAlias}.google_sub = ?`);
63
+ params.push(googleSub);
64
+ }
65
+
66
+ if (email) {
67
+ clauses.push(`${tableAlias}.email = ?`);
68
+ params.push(email);
69
+ }
70
+
71
+ if (ownerJid) {
72
+ clauses.push(`${tableAlias}.owner_jid = ?`);
73
+ params.push(ownerJid);
74
+ }
75
+
76
+ if (!clauses.length) return null;
77
+
78
+ return {
79
+ clause: clauses.join(' OR '),
80
+ params,
81
+ };
82
+ };
83
+
84
+ const mapCredentialRow = (row, { includeHash = false } = {}) => {
85
+ if (!row || typeof row !== 'object') return null;
86
+
87
+ const payload = {
88
+ google_sub: normalizeGoogleSubject(row.google_sub),
89
+ email: normalizeEmail(row.email),
90
+ owner_jid: normalizeJid(row.owner_jid),
91
+ name: normalizeName(row.name),
92
+ picture:
93
+ String(row.picture_url || '')
94
+ .trim()
95
+ .slice(0, 1024) || null,
96
+ password_algo:
97
+ String(row.password_algo || '')
98
+ .trim()
99
+ .toLowerCase() || 'argon2id',
100
+ password_cost: Math.max(0, Number(row.password_cost || 0)),
101
+ failed_attempts: Math.max(0, Number(row.failed_attempts || 0)),
102
+ last_failed_at: toIsoOrNull(row.last_failed_at),
103
+ last_login_at: toIsoOrNull(row.last_login_at),
104
+ password_changed_at: toIsoOrNull(row.password_changed_at),
105
+ revoked_at: toIsoOrNull(row.revoked_at),
106
+ created_at: toIsoOrNull(row.created_at),
107
+ updated_at: toIsoOrNull(row.updated_at),
108
+ has_password: Boolean(row.password_hash),
109
+ active: !row.revoked_at,
110
+ };
111
+
112
+ if (includeHash) {
113
+ payload.password_hash = String(row.password_hash || '');
114
+ }
115
+
116
+ return payload;
117
+ };
118
+
119
+ export const createUserPasswordAuthService = ({ executeQuery, tables = {}, logger = null, policy = {} } = {}) => {
120
+ if (typeof executeQuery !== 'function') {
121
+ throw new TypeError('createUserPasswordAuthService requer executeQuery valido.');
122
+ }
123
+
124
+ const resolvedPolicy = resolveUserPasswordPolicy(policy);
125
+
126
+ const GOOGLE_USER_TABLE = String(tables.STICKER_WEB_GOOGLE_USER || 'web_google_user').trim() || 'web_google_user';
127
+ const USER_PASSWORD_TABLE = String(tables.STICKER_WEB_USER_PASSWORD || 'web_user_password').trim() || 'web_user_password';
128
+ const maxFailedAttempts = clampInt(process.env.WEB_USER_PASSWORD_MAX_FAILED_ATTEMPTS, 8, 3, 100);
129
+ const lockoutSeconds = clampInt(process.env.WEB_USER_PASSWORD_LOCKOUT_SECONDS, 900, 30, 86_400);
130
+
131
+ const resolveCredentialLockState = (credential) => {
132
+ const failures = Math.max(0, Number(credential?.failed_attempts || 0));
133
+ if (failures < maxFailedAttempts) {
134
+ return { locked: false, retryAfterSeconds: 0 };
135
+ }
136
+
137
+ const lastFailedAtMs = Date.parse(String(credential?.last_failed_at || ''));
138
+ if (!Number.isFinite(lastFailedAtMs) || lastFailedAtMs <= 0) {
139
+ return { locked: false, retryAfterSeconds: 0 };
140
+ }
141
+
142
+ const elapsedSeconds = Math.max(0, Math.floor((Date.now() - lastFailedAtMs) / 1000));
143
+ const retryAfterSeconds = Math.max(0, lockoutSeconds - elapsedSeconds);
144
+ if (retryAfterSeconds <= 0) {
145
+ return { locked: false, retryAfterSeconds: 0 };
146
+ }
147
+
148
+ return { locked: true, retryAfterSeconds };
149
+ };
150
+
151
+ const findKnownGoogleUserByIdentity = async (identity = {}, connection = null) => {
152
+ const normalizedIdentity = normalizeIdentity(identity);
153
+ if (!normalizedIdentity.hasIdentity) return null;
154
+
155
+ const filter = buildIdentityFilterClause(normalizedIdentity, 'u');
156
+ if (!filter) return null;
157
+
158
+ const rows = await executeQuery(
159
+ `SELECT u.google_sub, u.email, u.owner_jid, u.name, u.picture_url, u.last_login_at, u.last_seen_at, u.created_at, u.updated_at
160
+ FROM ${GOOGLE_USER_TABLE} u
161
+ WHERE ${filter.clause}
162
+ ORDER BY COALESCE(u.last_seen_at, u.last_login_at, u.updated_at, u.created_at) DESC
163
+ LIMIT 1`,
164
+ filter.params,
165
+ connection,
166
+ );
167
+
168
+ const row = Array.isArray(rows) ? rows[0] : null;
169
+ if (!row) return null;
170
+
171
+ return {
172
+ google_sub: normalizeGoogleSubject(row.google_sub),
173
+ email: normalizeEmail(row.email),
174
+ owner_jid: normalizeJid(row.owner_jid),
175
+ name: normalizeName(row.name),
176
+ picture:
177
+ String(row.picture_url || '')
178
+ .trim()
179
+ .slice(0, 1024) || null,
180
+ last_login_at: toIsoOrNull(row.last_login_at),
181
+ last_seen_at: toIsoOrNull(row.last_seen_at),
182
+ created_at: toIsoOrNull(row.created_at),
183
+ updated_at: toIsoOrNull(row.updated_at),
184
+ };
185
+ };
186
+
187
+ const findCredentialByIdentityInternal = async (identity = {}, { includeRevoked = false, includeHash = false } = {}, connection = null) => {
188
+ const normalizedIdentity = normalizeIdentity(identity);
189
+ if (!normalizedIdentity.hasIdentity) return null;
190
+
191
+ const filter = buildIdentityFilterClause(normalizedIdentity, 'u');
192
+ if (!filter) return null;
193
+
194
+ const revokedClause = includeRevoked ? '' : 'AND p.revoked_at IS NULL';
195
+
196
+ const rows = await executeQuery(
197
+ `SELECT
198
+ p.google_sub,
199
+ p.password_hash,
200
+ p.password_algo,
201
+ p.password_cost,
202
+ p.failed_attempts,
203
+ p.last_failed_at,
204
+ p.last_login_at,
205
+ p.password_changed_at,
206
+ p.revoked_at,
207
+ p.created_at,
208
+ p.updated_at,
209
+ u.email,
210
+ u.owner_jid,
211
+ u.name,
212
+ u.picture_url
213
+ FROM ${USER_PASSWORD_TABLE} p
214
+ INNER JOIN ${GOOGLE_USER_TABLE} u ON u.google_sub = p.google_sub
215
+ WHERE (${filter.clause})
216
+ ${revokedClause}
217
+ ORDER BY p.updated_at DESC
218
+ LIMIT 1`,
219
+ filter.params,
220
+ connection,
221
+ );
222
+
223
+ return mapCredentialRow(Array.isArray(rows) ? rows[0] : null, { includeHash });
224
+ };
225
+
226
+ const touchCredentialSuccess = async (googleSub, connection = null) => {
227
+ const normalizedSub = normalizeGoogleSubject(googleSub);
228
+ if (!normalizedSub) return 0;
229
+
230
+ const result = await executeQuery(
231
+ `UPDATE ${USER_PASSWORD_TABLE}
232
+ SET failed_attempts = 0,
233
+ last_failed_at = NULL,
234
+ last_login_at = UTC_TIMESTAMP(),
235
+ updated_at = UTC_TIMESTAMP()
236
+ WHERE google_sub = ?
237
+ AND revoked_at IS NULL`,
238
+ [normalizedSub],
239
+ connection,
240
+ );
241
+
242
+ return Number(result?.affectedRows || 0);
243
+ };
244
+
245
+ const touchCredentialFailure = async (googleSub, connection = null) => {
246
+ const normalizedSub = normalizeGoogleSubject(googleSub);
247
+ if (!normalizedSub) return 0;
248
+
249
+ const result = await executeQuery(
250
+ `UPDATE ${USER_PASSWORD_TABLE}
251
+ SET failed_attempts = failed_attempts + 1,
252
+ last_failed_at = UTC_TIMESTAMP(),
253
+ updated_at = UTC_TIMESTAMP()
254
+ WHERE google_sub = ?
255
+ AND revoked_at IS NULL`,
256
+ [normalizedSub],
257
+ connection,
258
+ );
259
+
260
+ return Number(result?.affectedRows || 0);
261
+ };
262
+
263
+ const setPasswordForIdentity = async ({ googleSub = '', email = '', ownerJid = '', password = '' } = {}, connection = null) => {
264
+ const knownUser = await findKnownGoogleUserByIdentity({ googleSub, email, ownerJid }, connection);
265
+ if (!knownUser?.google_sub) {
266
+ throw buildHttpError('Usuario web nao encontrado. O usuario precisa autenticar no site antes de cadastrar senha.', {
267
+ statusCode: 404,
268
+ code: 'USER_NOT_FOUND',
269
+ });
270
+ }
271
+
272
+ const hashData = await hashUserPassword(password, resolvedPolicy);
273
+
274
+ await executeQuery(
275
+ `INSERT INTO ${USER_PASSWORD_TABLE}
276
+ (google_sub, password_hash, password_algo, password_cost, failed_attempts, last_failed_at, last_login_at, password_changed_at, revoked_at)
277
+ VALUES (?, ?, ?, ?, 0, NULL, NULL, UTC_TIMESTAMP(), NULL)
278
+ ON DUPLICATE KEY UPDATE
279
+ password_hash = VALUES(password_hash),
280
+ password_algo = VALUES(password_algo),
281
+ password_cost = VALUES(password_cost),
282
+ failed_attempts = 0,
283
+ last_failed_at = NULL,
284
+ password_changed_at = UTC_TIMESTAMP(),
285
+ revoked_at = NULL,
286
+ updated_at = UTC_TIMESTAMP()`,
287
+ [knownUser.google_sub, hashData.hash, hashData.algorithm, hashData.cost],
288
+ connection,
289
+ );
290
+
291
+ const credential = await findCredentialByIdentityInternal({ googleSub: knownUser.google_sub }, { includeRevoked: true }, connection);
292
+ if (!credential) {
293
+ throw buildHttpError('Falha ao salvar credencial de senha do usuario.', {
294
+ statusCode: 500,
295
+ code: 'PASSWORD_SAVE_FAILED',
296
+ });
297
+ }
298
+
299
+ if (logger && typeof logger.info === 'function') {
300
+ logger.info('Credencial de senha do usuario registrada/atualizada.', {
301
+ action: 'web_user_password_upsert',
302
+ google_sub: credential.google_sub,
303
+ });
304
+ }
305
+
306
+ return credential;
307
+ };
308
+
309
+ const verifyPasswordForIdentity = async ({ googleSub = '', email = '', ownerJid = '', password = '' } = {}, connection = null) => {
310
+ const rawPassword = typeof password === 'string' ? password : '';
311
+ if (!rawPassword) {
312
+ return {
313
+ authenticated: false,
314
+ reason: 'PASSWORD_REQUIRED',
315
+ credential: null,
316
+ };
317
+ }
318
+
319
+ const credentialWithHash = await findCredentialByIdentityInternal({ googleSub, email, ownerJid }, { includeRevoked: true, includeHash: true }, connection);
320
+ if (!credentialWithHash?.google_sub) {
321
+ return {
322
+ authenticated: false,
323
+ reason: 'CREDENTIAL_NOT_FOUND',
324
+ credential: null,
325
+ };
326
+ }
327
+
328
+ if (credentialWithHash.revoked_at) {
329
+ return {
330
+ authenticated: false,
331
+ reason: 'CREDENTIAL_REVOKED',
332
+ credential: mapCredentialRow(credentialWithHash),
333
+ };
334
+ }
335
+
336
+ const lockState = resolveCredentialLockState(credentialWithHash);
337
+ if (lockState.locked) {
338
+ return {
339
+ authenticated: false,
340
+ reason: 'ACCOUNT_LOCKED',
341
+ retryAfterSeconds: lockState.retryAfterSeconds,
342
+ credential: mapCredentialRow(credentialWithHash),
343
+ };
344
+ }
345
+
346
+ const isValid = await verifyUserPasswordHash(rawPassword, credentialWithHash.password_hash);
347
+
348
+ if (isValid) {
349
+ await touchCredentialSuccess(credentialWithHash.google_sub, connection);
350
+ const updatedCredential = await findCredentialByIdentityInternal({ googleSub: credentialWithHash.google_sub }, { includeRevoked: true }, connection);
351
+ return {
352
+ authenticated: true,
353
+ reason: null,
354
+ credential: updatedCredential,
355
+ };
356
+ }
357
+
358
+ await touchCredentialFailure(credentialWithHash.google_sub, connection);
359
+ const updatedCredential = await findCredentialByIdentityInternal({ googleSub: credentialWithHash.google_sub }, { includeRevoked: true }, connection);
360
+ const updatedLockState = resolveCredentialLockState(updatedCredential);
361
+
362
+ return {
363
+ authenticated: false,
364
+ reason: updatedLockState.locked ? 'ACCOUNT_LOCKED' : 'INVALID_PASSWORD',
365
+ retryAfterSeconds: updatedLockState.retryAfterSeconds,
366
+ credential: updatedCredential,
367
+ };
368
+ };
369
+
370
+ const revokePasswordForIdentity = async ({ googleSub = '', email = '', ownerJid = '' } = {}, connection = null) => {
371
+ const existing = await findCredentialByIdentityInternal({ googleSub, email, ownerJid }, { includeRevoked: true }, connection);
372
+ if (!existing?.google_sub) return null;
373
+
374
+ await executeQuery(
375
+ `UPDATE ${USER_PASSWORD_TABLE}
376
+ SET revoked_at = COALESCE(revoked_at, UTC_TIMESTAMP()),
377
+ updated_at = UTC_TIMESTAMP()
378
+ WHERE google_sub = ?`,
379
+ [existing.google_sub],
380
+ connection,
381
+ );
382
+
383
+ return findCredentialByIdentityInternal({ googleSub: existing.google_sub }, { includeRevoked: true }, connection);
384
+ };
385
+
386
+ const clearFailuresForIdentity = async ({ googleSub = '', email = '', ownerJid = '' } = {}, connection = null) => {
387
+ const existing = await findCredentialByIdentityInternal({ googleSub, email, ownerJid }, { includeRevoked: true }, connection);
388
+ if (!existing?.google_sub) return null;
389
+
390
+ await executeQuery(
391
+ `UPDATE ${USER_PASSWORD_TABLE}
392
+ SET failed_attempts = 0,
393
+ last_failed_at = NULL,
394
+ updated_at = UTC_TIMESTAMP()
395
+ WHERE google_sub = ?`,
396
+ [existing.google_sub],
397
+ connection,
398
+ );
399
+
400
+ return findCredentialByIdentityInternal({ googleSub: existing.google_sub }, { includeRevoked: true }, connection);
401
+ };
402
+
403
+ return {
404
+ policy: {
405
+ ...resolvedPolicy,
406
+ maxFailedAttempts,
407
+ lockoutSeconds,
408
+ },
409
+ getPolicy: () => ({
410
+ ...resolvedPolicy,
411
+ maxFailedAttempts,
412
+ lockoutSeconds,
413
+ }),
414
+ validatePassword: (password) => validateUserPassword(password, resolvedPolicy),
415
+ findKnownGoogleUserByIdentity,
416
+ findCredentialByIdentity: (identity = {}, options = {}, connection = null) => findCredentialByIdentityInternal(identity, options, connection),
417
+ setPasswordForIdentity,
418
+ verifyPasswordForIdentity,
419
+ revokePasswordForIdentity,
420
+ clearFailuresForIdentity,
421
+ };
422
+ };
@@ -0,0 +1,199 @@
1
+ import argon2 from 'argon2';
2
+
3
+ const clampInt = (value, fallback, min, max) => {
4
+ const numeric = Number(value);
5
+ if (!Number.isFinite(numeric)) return fallback;
6
+ return Math.max(min, Math.min(max, Math.floor(numeric)));
7
+ };
8
+
9
+ const parseEnvBool = (value, fallback = false) => {
10
+ if (value === undefined || value === null || value === '') return fallback;
11
+ const normalized = String(value).trim().toLowerCase();
12
+ if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
13
+ if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
14
+ return fallback;
15
+ };
16
+
17
+ const DEFAULT_ARGON2_TIME_COST = 3;
18
+ const MIN_ARGON2_TIME_COST = 2;
19
+ const MAX_ARGON2_TIME_COST = 10;
20
+
21
+ const DEFAULT_ARGON2_MEMORY_KB = 19_456;
22
+ const MIN_ARGON2_MEMORY_KB = 4_096;
23
+ const MAX_ARGON2_MEMORY_KB = 262_144;
24
+
25
+ const DEFAULT_ARGON2_PARALLELISM = 1;
26
+ const MIN_ARGON2_PARALLELISM = 1;
27
+ const MAX_ARGON2_PARALLELISM = 8;
28
+
29
+ const DEFAULT_ARGON2_HASH_LENGTH = 32;
30
+ const MIN_ARGON2_HASH_LENGTH = 16;
31
+ const MAX_ARGON2_HASH_LENGTH = 64;
32
+
33
+ const DEFAULT_MIN_LENGTH = 12;
34
+ const DEFAULT_MAX_LENGTH = 72;
35
+ const MIN_ALLOWED_LENGTH = 10;
36
+ const MAX_ALLOWED_LENGTH = 72;
37
+
38
+ const ARGON2_PREFIX = '$argon2id$';
39
+ const MIN_PEPPER_SECRET_LENGTH = 16;
40
+
41
+ const DEFAULT_POLICY_INPUT = {
42
+ argon2TimeCost: clampInt(process.env.WEB_USER_PASSWORD_ARGON2_TIME_COST, DEFAULT_ARGON2_TIME_COST, MIN_ARGON2_TIME_COST, MAX_ARGON2_TIME_COST),
43
+ argon2MemoryKb: clampInt(process.env.WEB_USER_PASSWORD_ARGON2_MEMORY_KB, DEFAULT_ARGON2_MEMORY_KB, MIN_ARGON2_MEMORY_KB, MAX_ARGON2_MEMORY_KB),
44
+ argon2Parallelism: clampInt(process.env.WEB_USER_PASSWORD_ARGON2_PARALLELISM, DEFAULT_ARGON2_PARALLELISM, MIN_ARGON2_PARALLELISM, MAX_ARGON2_PARALLELISM),
45
+ argon2HashLength: clampInt(process.env.WEB_USER_PASSWORD_ARGON2_HASH_LENGTH, DEFAULT_ARGON2_HASH_LENGTH, MIN_ARGON2_HASH_LENGTH, MAX_ARGON2_HASH_LENGTH),
46
+ minLength: clampInt(process.env.WEB_USER_PASSWORD_MIN_LENGTH, DEFAULT_MIN_LENGTH, MIN_ALLOWED_LENGTH, MAX_ALLOWED_LENGTH),
47
+ maxLength: clampInt(process.env.WEB_USER_PASSWORD_MAX_LENGTH, DEFAULT_MAX_LENGTH, MIN_ALLOWED_LENGTH, MAX_ALLOWED_LENGTH),
48
+ requireLetter: parseEnvBool(process.env.WEB_USER_PASSWORD_REQUIRE_LETTER, true),
49
+ requireNumber: parseEnvBool(process.env.WEB_USER_PASSWORD_REQUIRE_NUMBER, true),
50
+ };
51
+
52
+ const normalizePolicyLength = (minLength, maxLength) => {
53
+ const safeMin = clampInt(minLength, DEFAULT_MIN_LENGTH, MIN_ALLOWED_LENGTH, MAX_ALLOWED_LENGTH);
54
+ const safeMax = clampInt(maxLength, DEFAULT_MAX_LENGTH, MIN_ALLOWED_LENGTH, MAX_ALLOWED_LENGTH);
55
+
56
+ if (safeMin <= safeMax) {
57
+ return {
58
+ minLength: safeMin,
59
+ maxLength: safeMax,
60
+ };
61
+ }
62
+
63
+ return {
64
+ minLength: safeMax,
65
+ maxLength: safeMax,
66
+ };
67
+ };
68
+
69
+ export const resolveUserPasswordPolicy = (overrides = {}) => {
70
+ const merged = {
71
+ ...DEFAULT_POLICY_INPUT,
72
+ ...(overrides && typeof overrides === 'object' ? overrides : {}),
73
+ };
74
+
75
+ const lengthPolicy = normalizePolicyLength(merged.minLength, merged.maxLength);
76
+
77
+ return {
78
+ argon2TimeCost: clampInt(merged.argon2TimeCost ?? merged.bcryptRounds, DEFAULT_POLICY_INPUT.argon2TimeCost, MIN_ARGON2_TIME_COST, MAX_ARGON2_TIME_COST),
79
+ argon2MemoryKb: clampInt(merged.argon2MemoryKb, DEFAULT_POLICY_INPUT.argon2MemoryKb, MIN_ARGON2_MEMORY_KB, MAX_ARGON2_MEMORY_KB),
80
+ argon2Parallelism: clampInt(merged.argon2Parallelism, DEFAULT_POLICY_INPUT.argon2Parallelism, MIN_ARGON2_PARALLELISM, MAX_ARGON2_PARALLELISM),
81
+ argon2HashLength: clampInt(merged.argon2HashLength, DEFAULT_POLICY_INPUT.argon2HashLength, MIN_ARGON2_HASH_LENGTH, MAX_ARGON2_HASH_LENGTH),
82
+ // Compatibilidade temporaria para consumidores legados.
83
+ bcryptRounds: clampInt(merged.argon2TimeCost ?? merged.bcryptRounds, DEFAULT_POLICY_INPUT.argon2TimeCost, MIN_ARGON2_TIME_COST, MAX_ARGON2_TIME_COST),
84
+ minLength: lengthPolicy.minLength,
85
+ maxLength: lengthPolicy.maxLength,
86
+ requireLetter: Boolean(merged.requireLetter),
87
+ requireNumber: Boolean(merged.requireNumber),
88
+ };
89
+ };
90
+
91
+ export const DEFAULT_USER_PASSWORD_POLICY = resolveUserPasswordPolicy();
92
+
93
+ const buildValidationError = (errors) => {
94
+ const firstMessage = errors[0]?.message || 'Senha invalida.';
95
+ const error = new Error(firstMessage);
96
+ error.statusCode = 400;
97
+ error.code = 'INVALID_PASSWORD';
98
+ error.details = errors;
99
+ return error;
100
+ };
101
+
102
+ export const validateUserPassword = (password, policyOverrides = {}) => {
103
+ const policy = resolveUserPasswordPolicy(policyOverrides);
104
+ const rawPassword = typeof password === 'string' ? password : '';
105
+ const errors = [];
106
+
107
+ if (!rawPassword) {
108
+ errors.push({ code: 'PASSWORD_REQUIRED', message: 'Senha obrigatoria.' });
109
+ }
110
+
111
+ if (rawPassword && rawPassword.trim().length === 0) {
112
+ errors.push({
113
+ code: 'PASSWORD_WHITESPACE_ONLY',
114
+ message: 'Senha nao pode conter apenas espacos.',
115
+ });
116
+ }
117
+
118
+ if (rawPassword.length > 0 && rawPassword.length < policy.minLength) {
119
+ errors.push({
120
+ code: 'PASSWORD_TOO_SHORT',
121
+ message: `Senha deve ter no minimo ${policy.minLength} caracteres.`,
122
+ });
123
+ }
124
+
125
+ if (rawPassword.length > policy.maxLength) {
126
+ errors.push({
127
+ code: 'PASSWORD_TOO_LONG',
128
+ message: `Senha deve ter no maximo ${policy.maxLength} caracteres.`,
129
+ });
130
+ }
131
+
132
+ if (policy.requireLetter && rawPassword && !/[a-z]/i.test(rawPassword)) {
133
+ errors.push({
134
+ code: 'PASSWORD_LETTER_REQUIRED',
135
+ message: 'Senha deve conter pelo menos uma letra.',
136
+ });
137
+ }
138
+
139
+ if (policy.requireNumber && rawPassword && !/\d/.test(rawPassword)) {
140
+ errors.push({
141
+ code: 'PASSWORD_NUMBER_REQUIRED',
142
+ message: 'Senha deve conter pelo menos um numero.',
143
+ });
144
+ }
145
+
146
+ return {
147
+ valid: errors.length === 0,
148
+ errors,
149
+ policy,
150
+ };
151
+ };
152
+
153
+ const resolvePepperSecret = (overrideValue = '') => {
154
+ const secret = String(overrideValue || process.env.WEB_USER_PASSWORD_PEPPER_SECRET || '').trim();
155
+ if (secret.length >= MIN_PEPPER_SECRET_LENGTH) return secret;
156
+
157
+ const error = new Error(`WEB_USER_PASSWORD_PEPPER_SECRET deve ter no minimo ${MIN_PEPPER_SECRET_LENGTH} caracteres.`);
158
+ error.statusCode = 500;
159
+ error.code = 'PASSWORD_PEPPER_NOT_CONFIGURED';
160
+ throw error;
161
+ };
162
+
163
+ const buildPepperedPassword = (password, pepperSecret) => `${String(password || '')}${pepperSecret}`;
164
+
165
+ export const hashUserPassword = async (password, policyOverrides = {}, options = {}) => {
166
+ const validation = validateUserPassword(password, policyOverrides);
167
+ if (!validation.valid) {
168
+ throw buildValidationError(validation.errors);
169
+ }
170
+
171
+ const pepperSecret = resolvePepperSecret(options?.pepperSecret);
172
+ const hash = await argon2.hash(buildPepperedPassword(password, pepperSecret), {
173
+ type: argon2.argon2id,
174
+ timeCost: validation.policy.argon2TimeCost,
175
+ memoryCost: validation.policy.argon2MemoryKb,
176
+ parallelism: validation.policy.argon2Parallelism,
177
+ hashLength: validation.policy.argon2HashLength,
178
+ });
179
+
180
+ return {
181
+ hash,
182
+ algorithm: 'argon2id',
183
+ cost: validation.policy.argon2TimeCost,
184
+ policy: validation.policy,
185
+ };
186
+ };
187
+
188
+ export const verifyUserPasswordHash = async (password, passwordHash, options = {}) => {
189
+ const rawHash = String(passwordHash || '').trim();
190
+ if (!rawHash) return false;
191
+ if (!rawHash.startsWith(ARGON2_PREFIX)) return false;
192
+
193
+ try {
194
+ const pepperSecret = resolvePepperSecret(options?.pepperSecret);
195
+ return await argon2.verify(rawHash, buildPepperedPassword(password, pepperSecret));
196
+ } catch {
197
+ return false;
198
+ }
199
+ };
@@ -0,0 +1,76 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { hashUserPassword, resolveUserPasswordPolicy, validateUserPassword, verifyUserPasswordHash } from './userPasswordCrypto.js';
5
+
6
+ const TEST_PEPPER_SECRET = 'pepper-secreto-de-teste-argon2';
7
+ process.env.WEB_USER_PASSWORD_PEPPER_SECRET = TEST_PEPPER_SECRET;
8
+
9
+ test('resolveUserPasswordPolicy normaliza limites de Argon2 e tamanho', () => {
10
+ const policy = resolveUserPasswordPolicy({
11
+ argon2TimeCost: 99,
12
+ argon2MemoryKb: 999_999,
13
+ argon2Parallelism: 99,
14
+ argon2HashLength: 999,
15
+ minLength: 120,
16
+ maxLength: 4,
17
+ });
18
+
19
+ assert.equal(policy.argon2TimeCost, 10);
20
+ assert.equal(policy.argon2MemoryKb, 262_144);
21
+ assert.equal(policy.argon2Parallelism, 8);
22
+ assert.equal(policy.argon2HashLength, 64);
23
+ assert.equal(policy.minLength, 10);
24
+ assert.equal(policy.maxLength, 10);
25
+ });
26
+
27
+ test('validateUserPassword reprova senha curta', () => {
28
+ const result = validateUserPassword('abc', {
29
+ minLength: 10,
30
+ maxLength: 72,
31
+ });
32
+
33
+ assert.equal(result.valid, false);
34
+ assert.equal(
35
+ result.errors.some((item) => item.code === 'PASSWORD_TOO_SHORT'),
36
+ true,
37
+ );
38
+ });
39
+
40
+ test('hashUserPassword + verifyUserPasswordHash validam senha correta', async () => {
41
+ const password = 'SenhaInterna123';
42
+ const hashed = await hashUserPassword(password, {
43
+ argon2TimeCost: 2,
44
+ argon2MemoryKb: 4_096,
45
+ argon2Parallelism: 1,
46
+ argon2HashLength: 24,
47
+ minLength: 10,
48
+ maxLength: 72,
49
+ });
50
+
51
+ assert.equal(typeof hashed.hash, 'string');
52
+ assert.equal(hashed.hash.startsWith('$argon2id$'), true);
53
+ assert.equal(hashed.algorithm, 'argon2id');
54
+
55
+ const valid = await verifyUserPasswordHash(password, hashed.hash);
56
+ assert.equal(valid, true);
57
+ });
58
+
59
+ test('verifyUserPasswordHash retorna false para senha incorreta', async () => {
60
+ const hashed = await hashUserPassword('SenhaCorreta123', {
61
+ argon2TimeCost: 2,
62
+ argon2MemoryKb: 4_096,
63
+ argon2Parallelism: 1,
64
+ argon2HashLength: 24,
65
+ minLength: 10,
66
+ maxLength: 72,
67
+ });
68
+
69
+ const valid = await verifyUserPasswordHash('SenhaErrada', hashed.hash);
70
+ assert.equal(valid, false);
71
+ });
72
+
73
+ test('verifyUserPasswordHash retorna false para hash invalido', async () => {
74
+ const valid = await verifyUserPasswordHash('qualquer', 'nao-e-hash-argon2');
75
+ assert.equal(valid, false);
76
+ });