@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.
- package/.clusterfuzzlite/Dockerfile +10 -0
- package/.env.example +907 -0
- package/.github/codeql/codeql-config.yml +10 -0
- package/.github/dependabot.yml +35 -0
- package/.github/workflows/ci.yml +73 -0
- package/.github/workflows/codeql.yml +106 -0
- package/.github/workflows/db-migration-check.yml +98 -0
- package/.github/workflows/dependency-review.yml +22 -0
- package/.github/workflows/deploy.yml +95 -0
- package/.github/workflows/release.yml +106 -0
- package/.github/workflows/security-attest-provenance.yml +51 -0
- package/.github/workflows/security-gitleaks.yml +34 -0
- package/.github/workflows/security-runner-hardening.yml +31 -0
- package/.github/workflows/security-scorecard.yml +44 -0
- package/.github/workflows/security-zap-baseline.yml +44 -0
- package/.github/workflows/security-zap-full-scan.yml +43 -0
- package/.github/workflows/security-zizmor.yml +36 -0
- package/.github/workflows/wiki-sync.yml +44 -0
- package/.gitleaks.toml +15 -0
- package/.prettierrc +34 -0
- package/CODE_OF_CONDUCT.md +114 -0
- package/LICENSE +56 -0
- package/README.md +110 -0
- package/SECURITY.md +110 -0
- package/app/config/index.js +4 -0
- package/app/configParts/adminIdentity.js +92 -0
- package/app/configParts/baileysConfig.js +1818 -0
- package/app/configParts/groupUtils.js +692 -0
- package/app/configParts/loggerConfig.js +394 -0
- package/app/configParts/messagePersistenceService.js +305 -0
- package/app/connection/baileysCompatibility.test.js +40 -0
- package/app/connection/baileysDbAuthState.js +344 -0
- package/app/connection/socketController.js +2243 -0
- package/app/controllers/messageController.js +7 -0
- package/app/controllers/messagePipeline/commandMiddleware.js +146 -0
- package/app/controllers/messagePipeline/conversationMiddleware.js +183 -0
- package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +522 -0
- package/app/controllers/messagePipeline/postProcessingMiddleware.js +41 -0
- package/app/controllers/messagePipeline/preProcessingMiddlewares.js +166 -0
- package/app/controllers/messageProcessingPipeline.js +699 -0
- package/app/modules/adminModule/AGENT.md +4056 -0
- package/app/modules/adminModule/adminAiHelpService.js +56 -0
- package/app/modules/adminModule/adminConfigRuntime.js +177 -0
- package/app/modules/adminModule/commandConfig.json +7122 -0
- package/app/modules/adminModule/groupCommandHandlers.js +1823 -0
- package/app/modules/adminModule/groupCommandHandlers.test.js +350 -0
- package/app/modules/adminModule/groupEventHandlers.js +399 -0
- package/app/modules/aiModule/AGENT.md +547 -0
- package/app/modules/aiModule/aiAiHelpService.js +14 -0
- package/app/modules/aiModule/aiConfigRuntime.js +135 -0
- package/app/modules/aiModule/catCommand.js +967 -0
- package/app/modules/aiModule/commandConfig.json +981 -0
- package/app/modules/analyticsModule/messageAnalysisEventRepository.js +83 -0
- package/app/modules/gameModule/AGENT.md +196 -0
- package/app/modules/gameModule/commandConfig.json +366 -0
- package/app/modules/gameModule/diceCommand.js +42 -0
- package/app/modules/gameModule/gameAiHelpService.js +14 -0
- package/app/modules/gameModule/gameConfigRuntime.js +68 -0
- package/app/modules/menuModule/AGENT.md +205 -0
- package/app/modules/menuModule/commandConfig.json +366 -0
- package/app/modules/menuModule/common.js +316 -0
- package/app/modules/menuModule/menuAiHelpService.js +14 -0
- package/app/modules/menuModule/menuConfigRuntime.js +68 -0
- package/app/modules/menuModule/menus.js +66 -0
- package/app/modules/playModule/AGENT.md +321 -0
- package/app/modules/playModule/commandConfig.json +584 -0
- package/app/modules/playModule/playAiHelpService.js +14 -0
- package/app/modules/playModule/playCommand.js +1417 -0
- package/app/modules/playModule/playConfigRuntime.js +68 -0
- package/app/modules/quoteModule/AGENT.md +199 -0
- package/app/modules/quoteModule/commandConfig.json +366 -0
- package/app/modules/quoteModule/quoteAiHelpService.js +14 -0
- package/app/modules/quoteModule/quoteCommand.js +842 -0
- package/app/modules/quoteModule/quoteConfigRuntime.js +68 -0
- package/app/modules/rpgPokemonModule/AGENT.md +229 -0
- package/app/modules/rpgPokemonModule/commandConfig.json +386 -0
- package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +795 -0
- package/app/modules/rpgPokemonModule/rpgBattleService.js +2110 -0
- package/app/modules/rpgPokemonModule/rpgBattleService.test.js +770 -0
- package/app/modules/rpgPokemonModule/rpgEvolutionUtils.js +22 -0
- package/app/modules/rpgPokemonModule/rpgPokemonAiHelpService.js +14 -0
- package/app/modules/rpgPokemonModule/rpgPokemonCommand.js +174 -0
- package/app/modules/rpgPokemonModule/rpgPokemonConfigRuntime.js +68 -0
- package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +192 -0
- package/app/modules/rpgPokemonModule/rpgPokemonDomain.test.js +93 -0
- package/app/modules/rpgPokemonModule/rpgPokemonEvolution.test.js +46 -0
- package/app/modules/rpgPokemonModule/rpgPokemonMessages.js +746 -0
- package/app/modules/rpgPokemonModule/rpgPokemonRepository.js +1847 -0
- package/app/modules/rpgPokemonModule/rpgPokemonService.js +6839 -0
- package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +354 -0
- package/app/modules/statsModule/AGENT.md +320 -0
- package/app/modules/statsModule/commandConfig.json +540 -0
- package/app/modules/statsModule/globalRankingCommand.js +64 -0
- package/app/modules/statsModule/rankingCommand.js +41 -0
- package/app/modules/statsModule/rankingCommon.js +1305 -0
- package/app/modules/statsModule/statsAiHelpService.js +14 -0
- package/app/modules/statsModule/statsConfigRuntime.js +68 -0
- package/app/modules/stickerModule/AGENT.md +692 -0
- package/app/modules/stickerModule/addStickerMetadata.js +239 -0
- package/app/modules/stickerModule/commandConfig.json +1216 -0
- package/app/modules/stickerModule/convertToWebp.js +367 -0
- package/app/modules/stickerModule/stickerAiHelpService.js +14 -0
- package/app/modules/stickerModule/stickerCommand.js +446 -0
- package/app/modules/stickerModule/stickerConfigRuntime.js +68 -0
- package/app/modules/stickerModule/stickerConvertCommand.js +159 -0
- package/app/modules/stickerModule/stickerTextCommand.js +653 -0
- package/app/modules/stickerPackModule/AGENT.md +215 -0
- package/app/modules/stickerPackModule/autoPackCollectorRuntime.js +20 -0
- package/app/modules/stickerPackModule/autoPackCollectorService.js +357 -0
- package/app/modules/stickerPackModule/commandConfig.json +387 -0
- package/app/modules/stickerPackModule/domainEventOutboxRepository.js +227 -0
- package/app/modules/stickerPackModule/domainEvents.js +52 -0
- package/app/modules/stickerPackModule/semanticReclassificationEngine.js +429 -0
- package/app/modules/stickerPackModule/semanticReclassificationEngine.test.js +75 -0
- package/app/modules/stickerPackModule/semanticThemeClusterService.js +544 -0
- package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +400 -0
- package/app/modules/stickerPackModule/stickerAssetRepository.js +400 -0
- package/app/modules/stickerPackModule/stickerAssetReprocessQueueRepository.js +175 -0
- package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +3702 -0
- package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +559 -0
- package/app/modules/stickerPackModule/stickerClassificationService.js +557 -0
- package/app/modules/stickerPackModule/stickerDedicatedTaskWorkerRuntime.js +249 -0
- package/app/modules/stickerPackModule/stickerDomainEventBus.js +65 -0
- package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +208 -0
- package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +99 -0
- package/app/modules/stickerPackModule/stickerObjectStorageService.js +285 -0
- package/app/modules/stickerPackModule/stickerPackAiHelpService.js +14 -0
- package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +1148 -0
- package/app/modules/stickerPackModule/stickerPackConfigRuntime.js +68 -0
- package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +152 -0
- package/app/modules/stickerPackModule/stickerPackErrors.js +30 -0
- package/app/modules/stickerPackModule/stickerPackInteractionEventRepository.js +101 -0
- package/app/modules/stickerPackModule/stickerPackItemRepository.js +432 -0
- package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +313 -0
- package/app/modules/stickerPackModule/stickerPackMessageService.js +268 -0
- package/app/modules/stickerPackModule/stickerPackRepository.js +450 -0
- package/app/modules/stickerPackModule/stickerPackScoreSnapshotRepository.js +179 -0
- package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +271 -0
- package/app/modules/stickerPackModule/stickerPackService.js +733 -0
- package/app/modules/stickerPackModule/stickerPackServiceRuntime.js +32 -0
- package/app/modules/stickerPackModule/stickerPackUtils.js +107 -0
- package/app/modules/stickerPackModule/stickerStorageService.js +559 -0
- package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +242 -0
- package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +242 -0
- package/app/modules/systemMetricsModule/AGENT.md +193 -0
- package/app/modules/systemMetricsModule/commandConfig.json +344 -0
- package/app/modules/systemMetricsModule/pingCommand.js +399 -0
- package/app/modules/systemMetricsModule/systemMetricsAiHelpService.js +14 -0
- package/app/modules/systemMetricsModule/systemMetricsConfigRuntime.js +68 -0
- package/app/modules/tiktokModule/AGENT.md +196 -0
- package/app/modules/tiktokModule/commandConfig.json +366 -0
- package/app/modules/tiktokModule/tiktokAiHelpService.js +14 -0
- package/app/modules/tiktokModule/tiktokCommand.js +716 -0
- package/app/modules/tiktokModule/tiktokConfigRuntime.js +68 -0
- package/app/modules/userModule/AGENT.md +200 -0
- package/app/modules/userModule/commandConfig.json +386 -0
- package/app/modules/userModule/userAiHelpService.js +14 -0
- package/app/modules/userModule/userCommand.js +1155 -0
- package/app/modules/userModule/userConfigRuntime.js +68 -0
- package/app/modules/waifuPicsModule/AGENT.md +431 -0
- package/app/modules/waifuPicsModule/commandConfig.json +780 -0
- package/app/modules/waifuPicsModule/waifuPicsAiHelpService.js +14 -0
- package/app/modules/waifuPicsModule/waifuPicsCommand.js +586 -0
- package/app/modules/waifuPicsModule/waifuPicsConfigRuntime.js +68 -0
- package/app/observability/metrics.js +766 -0
- package/app/services/ai/aiHelpResponseCacheRepository.js +280 -0
- package/app/services/ai/aiLearningRepository.js +400 -0
- package/app/services/ai/commandConfigEnrichmentRepository.js +769 -0
- package/app/services/ai/commandConfigEnrichmentService.js +452 -0
- package/app/services/ai/commandConfigValidationService.js +443 -0
- package/app/services/ai/commandToolBuilderService.js +192 -0
- package/app/services/ai/conversationRouterService.js +516 -0
- package/app/services/ai/geminiService.js +115 -0
- package/app/services/ai/geminiService.test.js +87 -0
- package/app/services/ai/globalModuleAiHelpService.js +1412 -0
- package/app/services/ai/globalToolCallingService.js +203 -0
- package/app/services/ai/messageCommandExecutionService.js +391 -0
- package/app/services/ai/moduleAiHelpCoreService.js +1099 -0
- package/app/services/ai/moduleAiHelpWrapperFactory.js +65 -0
- package/app/services/ai/moduleCommandConfigRuntimeService.js +113 -0
- package/app/services/ai/moduleToolExecutorService.js +464 -0
- package/app/services/ai/moduleToolRegistryService.js +178 -0
- package/app/services/ai/toolCandidateSelectorService.js +781 -0
- package/app/services/auth/googleWebLinkService.js +80 -0
- package/app/services/auth/whatsappLoginLinkService.js +230 -0
- package/app/services/external/pokeApiService.js +398 -0
- package/app/services/group/groupMetadataService.js +311 -0
- package/app/services/infra/dbWriteQueue.js +874 -0
- package/app/services/infra/featureFlagService.js +131 -0
- package/app/services/infra/queueUtils.js +55 -0
- package/app/services/messaging/captchaService.js +491 -0
- package/app/services/messaging/messagePersistenceService.js +1 -0
- package/app/services/messaging/newsBroadcastService.js +347 -0
- package/app/services/sticker/stickerFocusService.js +347 -0
- package/app/services/sticker/stickerFocusService.test.js +43 -0
- package/app/store/aiPromptStore.js +38 -0
- package/app/store/conversationSessionStore.js +131 -0
- package/app/store/groupConfigStore.js +58 -0
- package/app/store/premiumUserStore.js +54 -0
- package/app/utils/antiLink/antiLinkModule.js +700 -0
- package/app/utils/http/getImageBufferModule.js +18 -0
- package/app/utils/json/jsonSanitizer.js +113 -0
- package/app/utils/json/jsonSanitizer.test.js +40 -0
- package/app/utils/systemMetrics/systemMetricsModule.js +88 -0
- package/app/workers/aiLearningWorker.js +605 -0
- package/app/workers/commandConfigEnrichmentWorker.js +242 -0
- package/database/index.js +2075 -0
- package/database/init.js +151 -0
- package/database/migrations/.gitkeep +0 -0
- package/database/migrations/20260307_d0_hardening_down.sql +64 -0
- package/database/migrations/20260307_d0_hardening_up.sql +79 -0
- package/database/migrations/20260307_d1_terms_acceptance_down.sql +11 -0
- package/database/migrations/20260307_d1_terms_acceptance_up.sql +37 -0
- package/database/migrations/20260307_d2_auth_hardening_down.sql +75 -0
- package/database/migrations/20260307_d2_auth_hardening_up.sql +100 -0
- package/database/migrations/20260314_d7_canonical_sender_down.sql +53 -0
- package/database/migrations/20260314_d7_canonical_sender_up.sql +114 -0
- package/database/migrations/20260406_d30_security_analytics_down.sql +95 -0
- package/database/migrations/20260406_d30_security_analytics_up.sql +292 -0
- package/database/migrations/20260407_d31_web_google_session_token_hardening_down.sql +2 -0
- package/database/migrations/20260407_d31_web_google_session_token_hardening_up.sql +17 -0
- package/database/migrations/20260408_d32_ai_help_response_cache_down.sql +1 -0
- package/database/migrations/20260408_d32_ai_help_response_cache_up.sql +22 -0
- package/database/migrations/20260409_d33_ai_learning_tables_down.sql +4 -0
- package/database/migrations/20260409_d33_ai_learning_tables_up.sql +52 -0
- package/database/migrations/20260410_d34_command_config_enrichment_down.sql +3 -0
- package/database/migrations/20260410_d34_command_config_enrichment_up.sql +48 -0
- package/database/schema.sql +1186 -0
- package/docker-compose.yml +104 -0
- package/docs/audits/stickerCatalogController-out-of-scope.md +103 -0
- package/docs/audits/stickerCatalogController-symbols.md +58 -0
- package/docs/compliance/acceptable-use-policy-2026-03-07.md +35 -0
- package/docs/compliance/dpa-b2b-standard-2026-03-07.md +80 -0
- package/docs/compliance/monthly-compliance-checklist-2026-03-07.md +88 -0
- package/docs/compliance/notice-and-takedown-policy-2026-03-07.md +34 -0
- package/docs/compliance/privacy-policy-2026-03-07.md +75 -0
- package/docs/compliance/subprocessors-inventory-2026-03-07.md +16 -0
- package/docs/database/production-db-evolution-runbook-2026q1.md +365 -0
- package/docs/security/dsar-lgpd-runbook-2026-03-07.md +86 -0
- package/docs/security/incident-response-lgpd-anpd-runbook-2026-03-07.md +77 -0
- package/docs/security/network-hardening-runbook-2026-03-07.md +137 -0
- package/docs/seo/omnizap-seo-playbook-br-2026-02-28.md +238 -0
- package/docs/seo/satellite-page-template.md +116 -0
- package/docs/seo/satellite-pages-phase1.json +364 -0
- package/docs/wiki/Home.md +120 -0
- package/docs/wiki/pair-extraordinaire-2026-03-08.md +3 -0
- package/docs/wiki/recent-changes-2026-03-08.md +47 -0
- package/ecosystem.prod.config.cjs +135 -0
- package/eslint.config.js +89 -0
- package/index.js +488 -0
- package/ml/clip_classifier/Dockerfile +18 -0
- package/ml/clip_classifier/README.md +118 -0
- package/ml/clip_classifier/adaptive_scoring.py +40 -0
- package/ml/clip_classifier/classifier.py +654 -0
- package/ml/clip_classifier/embedding_store.py +481 -0
- package/ml/clip_classifier/env_loader.py +15 -0
- package/ml/clip_classifier/llm_label_expander.py +144 -0
- package/ml/clip_classifier/main.py +213 -0
- package/ml/clip_classifier/requirements.txt +10 -0
- package/ml/clip_classifier/similarity_engine.py +74 -0
- package/new-logo.png +0 -0
- package/observability/alert-rules.yml +60 -0
- package/observability/grafana/dashboards/omnizap-mysql.json +136 -0
- package/observability/grafana/dashboards/omnizap-overview.json +170 -0
- package/observability/grafana/provisioning/dashboards/dashboards.yml +11 -0
- package/observability/grafana/provisioning/datasources/datasources.yml +15 -0
- package/observability/loki-config.yml +38 -0
- package/observability/mysql-setup.sql +46 -0
- package/observability/prometheus.yml +35 -0
- package/observability/promtail-config.yml +84 -0
- package/observability/sticker-catalog-slo.md +83 -0
- package/observability/sticker-scale-hardening-rollout.md +128 -0
- package/package.json +144 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/assets/css/commands-react.input.css +71 -0
- package/public/assets/css/create-pack-react.input.css +31 -0
- package/public/assets/css/home-react.input.css +106 -0
- package/public/assets/css/login-react.input.css +58 -0
- package/public/assets/css/stickers-react.input.css +18 -0
- package/public/assets/css/terms-react.input.css +115 -0
- package/public/assets/css/user-react.input.css +57 -0
- package/public/assets/images/brand-icon-192.png +0 -0
- package/public/assets/images/brand-logo-128.webp +0 -0
- package/public/assets/images/hero-banner-1280.jpg +0 -0
- package/public/comandos/commands-catalog.json +4517 -0
- package/public/css/api-docs.css +161 -0
- package/public/css/stickers-admin.css +1288 -0
- package/public/css/styles.css +679 -0
- package/public/css/systemadm/admin.css +474 -0
- package/public/css/systemadm/base.css +73 -0
- package/public/css/systemadm/components.css +662 -0
- package/public/css/systemadm/layout.css +229 -0
- package/public/css/systemadm/tokens.css +56 -0
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/js/apps/apiDocsApp.js +235 -0
- package/public/js/apps/commandsReactApp.js +528 -0
- package/public/js/apps/createPackApp.js +1646 -0
- package/public/js/apps/homeReactApp.js +942 -0
- package/public/js/apps/loginReactApp.js +496 -0
- package/public/js/apps/stickersAdminApp.js +1753 -0
- package/public/js/apps/stickersApp.js +3797 -0
- package/public/js/apps/termsReactApp.js +528 -0
- package/public/js/apps/userApp.js +2540 -0
- package/public/js/apps/userProfile/actions.js +66 -0
- package/public/js/apps/userReactApp.js +547 -0
- package/public/js/catalog.js +950 -0
- package/public/pages/api-docs.html +40 -0
- package/public/pages/aup.html +158 -0
- package/public/pages/comandos.html +41 -0
- package/public/pages/dpa.html +227 -0
- package/public/pages/home.html +45 -0
- package/public/pages/licenca.html +182 -0
- package/public/pages/login.html +40 -0
- package/public/pages/notice-and-takedown.html +234 -0
- package/public/pages/politica-de-privacidade.html +251 -0
- package/public/pages/seo-bot-whatsapp-para-grupo.html +350 -0
- package/public/pages/seo-bot-whatsapp-sem-programar.html +350 -0
- package/public/pages/seo-como-automatizar-avisos-no-whatsapp.html +350 -0
- package/public/pages/seo-como-criar-comandos-whatsapp.html +350 -0
- package/public/pages/seo-como-evitar-spam-no-whatsapp.html +350 -0
- package/public/pages/seo-como-moderar-grupo-whatsapp.html +350 -0
- package/public/pages/seo-como-organizar-comunidade-whatsapp.html +350 -0
- package/public/pages/seo-melhor-bot-whatsapp-para-grupos.html +350 -0
- package/public/pages/stickers-admin.html +31 -0
- package/public/pages/stickers-create.html +41 -0
- package/public/pages/stickers.html +45 -0
- package/public/pages/suboperadores.html +237 -0
- package/public/pages/termos-de-uso-texto-integral.html +241 -0
- package/public/pages/termos-de-uso.html +41 -0
- package/public/pages/user-password-reset.html +32 -0
- package/public/pages/user-systemadm.html +508 -0
- package/public/pages/user.html +39 -0
- package/public/robots.txt +9 -0
- package/public/site.webmanifest +24 -0
- package/public/sitemap.xml +98 -0
- package/schemas/command-config.schema.json +582 -0
- package/scripts/baileys-compat-smoke.mjs +12 -0
- package/scripts/cache-bust.mjs +142 -0
- package/scripts/deploy.sh +916 -0
- package/scripts/email-broadcast-terms-update.mjs +170 -0
- package/scripts/enrich-command-discovery-fields.mjs +286 -0
- package/scripts/generate-command-config-schema.mjs +273 -0
- package/scripts/generate-commands-catalog.mjs +308 -0
- package/scripts/generate-module-agents.mjs +631 -0
- package/scripts/generate-seo-satellite-pages.mjs +400 -0
- package/scripts/github-deploy-notify.mjs +174 -0
- package/scripts/github-release-notify.mjs +219 -0
- package/scripts/release.sh +599 -0
- package/scripts/run-codeql-local.sh +116 -0
- package/scripts/run-prettier-all.mjs +25 -0
- package/scripts/security-smoketest.mjs +581 -0
- package/scripts/sticker-catalog-loadtest.mjs +210 -0
- package/scripts/sticker-worker-task.mjs +119 -0
- package/scripts/sync-readme-snapshot.mjs +133 -0
- package/scripts/validate-command-config-schema.mjs +130 -0
- package/scripts/validate-command-configs.mjs +15 -0
- package/scripts/wiki-sync.sh +191 -0
- package/server/auth/googleWebAuth/googleWebAuthRuntime.js +62 -0
- package/server/auth/googleWebAuth/googleWebAuthService.js +807 -0
- package/server/auth/jwt/webJwtService.js +147 -0
- package/server/auth/stickerCatalogAuthContext.js +165 -0
- package/server/auth/termsAcceptance/termsAcceptanceHandler.js +189 -0
- package/server/auth/userPassword/index.js +14 -0
- package/server/auth/userPassword/userPasswordAuthService.js +422 -0
- package/server/auth/userPassword/userPasswordCrypto.js +199 -0
- package/server/auth/userPassword/userPasswordCrypto.test.js +76 -0
- package/server/auth/userPassword/userPasswordRecoveryService.js +728 -0
- package/server/auth/validation/authSchemas.js +236 -0
- package/server/auth/webAccount/webAccountHandlers.js +1434 -0
- package/server/controllers/admin/adminBanService.js +138 -0
- package/server/controllers/admin/adminPanelHandlers.js +2083 -0
- package/server/controllers/admin/stickerCatalogAdminContext.js +17 -0
- package/server/controllers/admin/systemAdminController.js +201 -0
- package/server/controllers/email/emailAutomationController.js +239 -0
- package/server/controllers/metricsController.js +21 -0
- package/server/controllers/seo/stickerCatalogSeoContext.js +514 -0
- package/server/controllers/sticker/nonCatalogHandlers.js +303 -0
- package/server/controllers/sticker/stickerCatalogController.js +4700 -0
- package/server/controllers/system/contactController.js +115 -0
- package/server/controllers/system/githubController.js +137 -0
- package/server/controllers/system/stickerCatalogSystemContext.js +758 -0
- package/server/controllers/system/storageController.js +154 -0
- package/server/controllers/system/systemController.js +135 -0
- package/server/controllers/system/systemMetricsController.js +156 -0
- package/server/controllers/system/visitController.js +90 -0
- package/server/controllers/userController.js +145 -0
- package/server/email/emailAutomationRuntime.js +225 -0
- package/server/email/emailAutomationService.js +125 -0
- package/server/email/emailOutboxRepository.js +282 -0
- package/server/email/emailTemplateService.js +480 -0
- package/server/email/emailTransportService.js +156 -0
- package/server/http/clientIp.js +95 -0
- package/server/http/httpRequestUtils.js +262 -0
- package/server/http/httpRequestUtils.test.js +80 -0
- package/server/http/httpServer.js +180 -0
- package/server/http/requestContext.js +20 -0
- package/server/http/siteRoutingUtils.js +87 -0
- package/server/index.js +1 -0
- package/server/middleware/cachePolicy.js +26 -0
- package/server/middleware/cachePolicyHelpers.js +1 -0
- package/server/middleware/endpointRateLimit.js +181 -0
- package/server/middleware/rateLimit.js +70 -0
- package/server/middleware/requireAdminAuth.js +48 -0
- package/server/middleware/securityHeaders.js +97 -0
- package/server/routes/admin/systemAdminRouter.js +64 -0
- package/server/routes/email/emailAutomationRouter.js +46 -0
- package/server/routes/health/healthRouter.js +41 -0
- package/server/routes/indexRouter.js +234 -0
- package/server/routes/metrics/metricsRouter.js +58 -0
- package/server/routes/static/staticPageRouter.js +134 -0
- package/server/routes/sticker/catalogHandlers/catalogAdminHttp.js +105 -0
- package/server/routes/sticker/catalogHandlers/catalogAuthHttp.js +77 -0
- package/server/routes/sticker/catalogHandlers/catalogPublicHttp.js +120 -0
- package/server/routes/sticker/catalogHandlers/catalogUploadHttp.js +83 -0
- package/server/routes/sticker/catalogRouter.js +77 -0
- package/server/routes/sticker/stickerApiRouter.js +84 -0
- package/server/routes/sticker/stickerDataRouter.js +145 -0
- package/server/routes/sticker/stickerSiteRouter.js +43 -0
- package/server/routes/user/userApiPaths.js +66 -0
- package/server/routes/user/userRouter.js +65 -0
- package/server/utils/safePath.js +26 -0
- package/utils/logger/loggerModule.js +35 -0
- package/vite.config.mjs +38 -0
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
import { createHash, pbkdf2 as pbkdf2Callback, randomInt, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
|
|
4
|
+
const pbkdf2 = promisify(pbkdf2Callback);
|
|
5
|
+
|
|
6
|
+
const clampInt = (value, fallback, min, max) => {
|
|
7
|
+
const numeric = Number(value);
|
|
8
|
+
if (!Number.isFinite(numeric)) return fallback;
|
|
9
|
+
return Math.max(min, Math.min(max, Math.floor(numeric)));
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const normalizeGoogleSubject = (value) =>
|
|
13
|
+
String(value || '')
|
|
14
|
+
.trim()
|
|
15
|
+
.replace(/[^a-zA-Z0-9_-]/g, '')
|
|
16
|
+
.slice(0, 80);
|
|
17
|
+
|
|
18
|
+
const normalizeEmail = (value) =>
|
|
19
|
+
String(value || '')
|
|
20
|
+
.trim()
|
|
21
|
+
.toLowerCase()
|
|
22
|
+
.slice(0, 255);
|
|
23
|
+
|
|
24
|
+
const normalizeJid = (value) =>
|
|
25
|
+
String(value || '')
|
|
26
|
+
.trim()
|
|
27
|
+
.toLowerCase()
|
|
28
|
+
.slice(0, 255);
|
|
29
|
+
|
|
30
|
+
const normalizePurpose = (value) => {
|
|
31
|
+
const normalized = String(value || '')
|
|
32
|
+
.trim()
|
|
33
|
+
.toLowerCase();
|
|
34
|
+
if (normalized === 'setup') return 'setup';
|
|
35
|
+
return 'reset';
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const normalizeIp = (value) =>
|
|
39
|
+
String(value || '')
|
|
40
|
+
.trim()
|
|
41
|
+
.slice(0, 64);
|
|
42
|
+
|
|
43
|
+
const normalizeUserAgent = (value) =>
|
|
44
|
+
String(value || '')
|
|
45
|
+
.trim()
|
|
46
|
+
.slice(0, 255);
|
|
47
|
+
|
|
48
|
+
const normalizeCode = (value) => {
|
|
49
|
+
const digits = String(value || '').replace(/\D+/g, '');
|
|
50
|
+
if (!digits) return '';
|
|
51
|
+
return digits.slice(0, 6);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const maskEmail = (value) => {
|
|
55
|
+
const normalized = normalizeEmail(value);
|
|
56
|
+
if (!normalized || !normalized.includes('@')) return null;
|
|
57
|
+
const [localRaw, domainRaw] = normalized.split('@');
|
|
58
|
+
const local = String(localRaw || '');
|
|
59
|
+
const domain = String(domainRaw || '');
|
|
60
|
+
if (!local || !domain) return null;
|
|
61
|
+
const maskedLocal = local.length <= 2 ? `${local.charAt(0) || '*'}*` : `${local.slice(0, 2)}***`;
|
|
62
|
+
const [domainHead, ...domainRest] = domain.split('.');
|
|
63
|
+
const maskedDomainHead = domainHead.length <= 2 ? `${domainHead.charAt(0) || '*'}*` : `${domainHead.slice(0, 2)}***`;
|
|
64
|
+
const domainSuffix = domainRest.length ? `.${domainRest.join('.')}` : '';
|
|
65
|
+
return `${maskedLocal}@${maskedDomainHead}${domainSuffix}`;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const buildHttpError = (message, { statusCode = 400, code = 'BAD_REQUEST', details = undefined } = {}) => {
|
|
69
|
+
const error = new Error(String(message || 'Erro interno.'));
|
|
70
|
+
error.statusCode = Number(statusCode) || 400;
|
|
71
|
+
error.code = String(code || 'BAD_REQUEST');
|
|
72
|
+
if (details !== undefined) {
|
|
73
|
+
error.details = details;
|
|
74
|
+
}
|
|
75
|
+
return error;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const normalizeIdentity = ({ googleSub = '', email = '', ownerJid = '' } = {}) => {
|
|
79
|
+
const normalizedGoogleSub = normalizeGoogleSubject(googleSub);
|
|
80
|
+
const normalizedEmail = normalizeEmail(email);
|
|
81
|
+
const normalizedOwnerJid = normalizeJid(ownerJid);
|
|
82
|
+
return {
|
|
83
|
+
googleSub: normalizedGoogleSub,
|
|
84
|
+
email: normalizedEmail,
|
|
85
|
+
ownerJid: normalizedOwnerJid,
|
|
86
|
+
hasIdentity: Boolean(normalizedGoogleSub || normalizedEmail || normalizedOwnerJid),
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const buildIdentityFilterClause = ({ googleSub = '', email = '', ownerJid = '' } = {}, tableAlias = 'c') => {
|
|
91
|
+
const clauses = [];
|
|
92
|
+
const params = [];
|
|
93
|
+
|
|
94
|
+
if (googleSub) {
|
|
95
|
+
clauses.push(`${tableAlias}.google_sub = ?`);
|
|
96
|
+
params.push(googleSub);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (email) {
|
|
100
|
+
clauses.push(`${tableAlias}.email = ?`);
|
|
101
|
+
params.push(email);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (ownerJid) {
|
|
105
|
+
clauses.push(`${tableAlias}.owner_jid = ?`);
|
|
106
|
+
params.push(ownerJid);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!clauses.length) return null;
|
|
110
|
+
return {
|
|
111
|
+
clause: clauses.join(' OR '),
|
|
112
|
+
params,
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const normalizeRecoveryRow = (row) => {
|
|
117
|
+
if (!row || typeof row !== 'object') return null;
|
|
118
|
+
return {
|
|
119
|
+
id: Number(row.id || 0),
|
|
120
|
+
google_sub: normalizeGoogleSubject(row.google_sub),
|
|
121
|
+
email: normalizeEmail(row.email),
|
|
122
|
+
owner_jid: normalizeJid(row.owner_jid),
|
|
123
|
+
purpose: normalizePurpose(row.purpose),
|
|
124
|
+
code_hash: String(row.code_hash || '')
|
|
125
|
+
.trim()
|
|
126
|
+
.toLowerCase()
|
|
127
|
+
.slice(0, 64),
|
|
128
|
+
attempts: Math.max(0, Number(row.attempts || 0)),
|
|
129
|
+
max_attempts: Math.max(1, Number(row.max_attempts || 1)),
|
|
130
|
+
expires_at: row.expires_at ? new Date(row.expires_at).toISOString() : null,
|
|
131
|
+
consumed_at: row.consumed_at ? new Date(row.consumed_at).toISOString() : null,
|
|
132
|
+
revoked_at: row.revoked_at ? new Date(row.revoked_at).toISOString() : null,
|
|
133
|
+
created_at: row.created_at ? new Date(row.created_at).toISOString() : null,
|
|
134
|
+
last_attempt_at: row.last_attempt_at ? new Date(row.last_attempt_at).toISOString() : null,
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const generateSixDigitCode = () => String(randomInt(0, 1_000_000)).padStart(6, '0');
|
|
139
|
+
|
|
140
|
+
const secureHexEquals = (leftHex, rightHex) => {
|
|
141
|
+
const left = String(leftHex || '').trim();
|
|
142
|
+
const right = String(rightHex || '').trim();
|
|
143
|
+
if (!left || !right) return false;
|
|
144
|
+
|
|
145
|
+
let leftBuffer;
|
|
146
|
+
let rightBuffer;
|
|
147
|
+
try {
|
|
148
|
+
leftBuffer = Buffer.from(left, 'hex');
|
|
149
|
+
rightBuffer = Buffer.from(right, 'hex');
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!leftBuffer.length || !rightBuffer.length || leftBuffer.length !== rightBuffer.length) return false;
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
return timingSafeEqual(leftBuffer, rightBuffer);
|
|
158
|
+
} catch {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const DEFAULT_CODE_TTL_SECONDS = 15 * 60;
|
|
164
|
+
const DEFAULT_MAX_ATTEMPTS = 5;
|
|
165
|
+
const DEFAULT_RESEND_COOLDOWN_SECONDS = 60;
|
|
166
|
+
const DEFAULT_HOURLY_REQUEST_LIMIT = 4;
|
|
167
|
+
const DEFAULT_DAILY_REQUEST_LIMIT = 8;
|
|
168
|
+
const DEFAULT_RECOVERY_HASH_ITERATIONS = 210_000;
|
|
169
|
+
const MIN_RECOVERY_HASH_ITERATIONS = 100_000;
|
|
170
|
+
const MAX_RECOVERY_HASH_ITERATIONS = 2_000_000;
|
|
171
|
+
const RECOVERY_HASH_KEYLEN_BYTES = 32;
|
|
172
|
+
|
|
173
|
+
export const createUserPasswordRecoveryService = ({ executeQuery, userPasswordAuthService, queueAutomatedEmail = null, revokeWebSessionsByIdentity = null, tables = {}, logger = null, runSqlTransaction = null } = {}) => {
|
|
174
|
+
if (typeof executeQuery !== 'function') {
|
|
175
|
+
throw new TypeError('createUserPasswordRecoveryService requer executeQuery valido.');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!userPasswordAuthService || typeof userPasswordAuthService.findKnownGoogleUserByIdentity !== 'function' || typeof userPasswordAuthService.setPasswordForIdentity !== 'function' || typeof userPasswordAuthService.validatePassword !== 'function') {
|
|
179
|
+
throw new TypeError('createUserPasswordRecoveryService requer userPasswordAuthService valido.');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const recoveryTable = String(tables.STICKER_WEB_USER_PASSWORD_RECOVERY_CODE || 'web_user_password_recovery_code').trim() || 'web_user_password_recovery_code';
|
|
183
|
+
const ttlSeconds = clampInt(process.env.WEB_USER_PASSWORD_RECOVERY_CODE_TTL_SECONDS, DEFAULT_CODE_TTL_SECONDS, 180, 60 * 60);
|
|
184
|
+
const maxAttempts = clampInt(process.env.WEB_USER_PASSWORD_RECOVERY_CODE_MAX_ATTEMPTS, DEFAULT_MAX_ATTEMPTS, 1, 10);
|
|
185
|
+
const resendCooldownSeconds = clampInt(process.env.WEB_USER_PASSWORD_RECOVERY_CODE_RESEND_COOLDOWN_SECONDS, DEFAULT_RESEND_COOLDOWN_SECONDS, 15, 10 * 60);
|
|
186
|
+
const hourlyRequestLimit = clampInt(process.env.WEB_USER_PASSWORD_RECOVERY_CODE_HOURLY_LIMIT, DEFAULT_HOURLY_REQUEST_LIMIT, 1, 30);
|
|
187
|
+
const dailyRequestLimit = clampInt(process.env.WEB_USER_PASSWORD_RECOVERY_CODE_DAILY_LIMIT, DEFAULT_DAILY_REQUEST_LIMIT, 1, 40);
|
|
188
|
+
const recoveryHashIterations = clampInt(process.env.WEB_USER_PASSWORD_RECOVERY_HASH_ITERATIONS, DEFAULT_RECOVERY_HASH_ITERATIONS, MIN_RECOVERY_HASH_ITERATIONS, MAX_RECOVERY_HASH_ITERATIONS);
|
|
189
|
+
const hashSecret =
|
|
190
|
+
String(process.env.WEB_USER_PASSWORD_RECOVERY_HASH_SECRET || process.env.WEB_AUTH_JWT_SECRET || process.env.WHATSAPP_LOGIN_LINK_SECRET || '')
|
|
191
|
+
.trim()
|
|
192
|
+
.slice(0, 512) || randomUUID();
|
|
193
|
+
|
|
194
|
+
if (!process.env.WEB_USER_PASSWORD_RECOVERY_HASH_SECRET && !process.env.WEB_AUTH_JWT_SECRET && !process.env.WHATSAPP_LOGIN_LINK_SECRET && logger && typeof logger.warn === 'function') {
|
|
195
|
+
logger.warn('Segredo dedicado de recuperacao de senha nao configurado. Usando segredo efemero em memoria.', {
|
|
196
|
+
action: 'web_user_password_recovery_secret_fallback',
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const withTransaction = async (handler) => {
|
|
201
|
+
if (typeof runSqlTransaction === 'function') {
|
|
202
|
+
return runSqlTransaction(handler);
|
|
203
|
+
}
|
|
204
|
+
return handler(null);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const buildSensitiveMetadataHash = (value, { scope = 'generic' } = {}) => {
|
|
208
|
+
const normalizedValue = String(value || '').trim();
|
|
209
|
+
if (!normalizedValue) return null;
|
|
210
|
+
return createHash('sha256').update(`${hashSecret}|recovery_sensitive|${scope}|${normalizedValue}`).digest();
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const buildCodeHashV2 = async ({ code = '', googleSub = '', email = '', purpose = 'reset' } = {}) => {
|
|
214
|
+
const normalizedPurpose = normalizePurpose(purpose);
|
|
215
|
+
const normalizedGoogleSub = normalizeGoogleSubject(googleSub);
|
|
216
|
+
const normalizedEmail = normalizeEmail(email);
|
|
217
|
+
const normalizedCode = normalizeCode(code);
|
|
218
|
+
|
|
219
|
+
const material = `${normalizedPurpose}|${normalizedGoogleSub}|${normalizedEmail}|${normalizedCode}`;
|
|
220
|
+
const salt = `${hashSecret}|web_user_password_recovery_code|v2`;
|
|
221
|
+
const derived = await pbkdf2(material, salt, recoveryHashIterations, RECOVERY_HASH_KEYLEN_BYTES, 'sha256');
|
|
222
|
+
return Buffer.from(derived).toString('hex');
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const buildCodeHashV3 = async ({ code = '', googleSub = '', purpose = 'reset' } = {}) => {
|
|
226
|
+
const normalizedPurpose = normalizePurpose(purpose);
|
|
227
|
+
const normalizedGoogleSub = normalizeGoogleSubject(googleSub);
|
|
228
|
+
const normalizedCode = normalizeCode(code);
|
|
229
|
+
|
|
230
|
+
const material = `${normalizedPurpose}|${normalizedGoogleSub}|${normalizedCode}`;
|
|
231
|
+
const salt = `${hashSecret}|web_user_password_recovery_code|v3`;
|
|
232
|
+
const derived = await pbkdf2(material, salt, recoveryHashIterations, RECOVERY_HASH_KEYLEN_BYTES, 'sha256');
|
|
233
|
+
return Buffer.from(derived).toString('hex');
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const findLatestActiveCodeByIdentity = async ({ googleSub = '', email = '', ownerJid = '', purpose = '' } = {}, connection = null) => {
|
|
237
|
+
const normalizedIdentity = normalizeIdentity({ googleSub, email, ownerJid });
|
|
238
|
+
if (!normalizedIdentity.hasIdentity) return null;
|
|
239
|
+
|
|
240
|
+
const filter = buildIdentityFilterClause(normalizedIdentity, 'c');
|
|
241
|
+
if (!filter) return null;
|
|
242
|
+
|
|
243
|
+
const normalizedPurpose = String(purpose || '')
|
|
244
|
+
.trim()
|
|
245
|
+
.toLowerCase();
|
|
246
|
+
const purposeClause = normalizedPurpose ? 'AND c.purpose = ?' : '';
|
|
247
|
+
const params = normalizedPurpose ? [...filter.params, normalizedPurpose] : [...filter.params];
|
|
248
|
+
|
|
249
|
+
const rows = await executeQuery(
|
|
250
|
+
`SELECT
|
|
251
|
+
c.id,
|
|
252
|
+
c.google_sub,
|
|
253
|
+
c.email,
|
|
254
|
+
c.owner_jid,
|
|
255
|
+
c.purpose,
|
|
256
|
+
c.code_hash,
|
|
257
|
+
c.attempts,
|
|
258
|
+
c.max_attempts,
|
|
259
|
+
c.last_attempt_at,
|
|
260
|
+
c.expires_at,
|
|
261
|
+
c.consumed_at,
|
|
262
|
+
c.revoked_at,
|
|
263
|
+
c.created_at
|
|
264
|
+
FROM ${recoveryTable} c
|
|
265
|
+
WHERE (${filter.clause})
|
|
266
|
+
${purposeClause}
|
|
267
|
+
AND c.revoked_at IS NULL
|
|
268
|
+
AND c.consumed_at IS NULL
|
|
269
|
+
AND c.expires_at > UTC_TIMESTAMP()
|
|
270
|
+
ORDER BY c.id DESC
|
|
271
|
+
LIMIT 1`,
|
|
272
|
+
params,
|
|
273
|
+
connection,
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
return normalizeRecoveryRow(Array.isArray(rows) ? rows[0] : null);
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const countRequestsByGoogleSubInWindow = async (googleSub, windowSeconds, connection = null) => {
|
|
280
|
+
const normalizedGoogleSub = normalizeGoogleSubject(googleSub);
|
|
281
|
+
if (!normalizedGoogleSub) return 0;
|
|
282
|
+
const safeWindowSeconds = clampInt(windowSeconds, 24 * 60 * 60, 60, 7 * 24 * 60 * 60);
|
|
283
|
+
|
|
284
|
+
const rows = await executeQuery(
|
|
285
|
+
`SELECT COUNT(*) AS total
|
|
286
|
+
FROM ${recoveryTable}
|
|
287
|
+
WHERE google_sub = ?
|
|
288
|
+
AND created_at >= (UTC_TIMESTAMP() - INTERVAL ${safeWindowSeconds} SECOND)`,
|
|
289
|
+
[normalizedGoogleSub],
|
|
290
|
+
connection,
|
|
291
|
+
);
|
|
292
|
+
return Math.max(0, Number(rows?.[0]?.total || 0));
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const countDailyRequestsByGoogleSub = async (googleSub, connection = null) => countRequestsByGoogleSubInWindow(googleSub, 24 * 60 * 60, connection);
|
|
296
|
+
|
|
297
|
+
const countHourlyRequestsByGoogleSub = async (googleSub, connection = null) => countRequestsByGoogleSubInWindow(googleSub, 60 * 60, connection);
|
|
298
|
+
|
|
299
|
+
const getRetryAfterForWindowByGoogleSub = async (googleSub, windowSeconds, connection = null) => {
|
|
300
|
+
const normalizedGoogleSub = normalizeGoogleSubject(googleSub);
|
|
301
|
+
if (!normalizedGoogleSub) return clampInt(windowSeconds, 60, 1, 7 * 24 * 60 * 60);
|
|
302
|
+
const safeWindowSeconds = clampInt(windowSeconds, 24 * 60 * 60, 60, 7 * 24 * 60 * 60);
|
|
303
|
+
|
|
304
|
+
const rows = await executeQuery(
|
|
305
|
+
`SELECT MIN(created_at) AS oldest_created_at
|
|
306
|
+
FROM ${recoveryTable}
|
|
307
|
+
WHERE google_sub = ?
|
|
308
|
+
AND created_at >= (UTC_TIMESTAMP() - INTERVAL ${safeWindowSeconds} SECOND)`,
|
|
309
|
+
[normalizedGoogleSub],
|
|
310
|
+
connection,
|
|
311
|
+
);
|
|
312
|
+
const oldestCreatedAt = rows?.[0]?.oldest_created_at ? Date.parse(rows[0].oldest_created_at) : NaN;
|
|
313
|
+
if (!Number.isFinite(oldestCreatedAt)) return safeWindowSeconds;
|
|
314
|
+
|
|
315
|
+
const elapsedSeconds = Math.max(0, Math.floor((Date.now() - oldestCreatedAt) / 1000));
|
|
316
|
+
return Math.max(1, safeWindowSeconds - elapsedSeconds);
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const getRecentRequestWithinCooldown = async (googleSub, connection = null) => {
|
|
320
|
+
const normalizedGoogleSub = normalizeGoogleSubject(googleSub);
|
|
321
|
+
if (!normalizedGoogleSub) return null;
|
|
322
|
+
|
|
323
|
+
const rows = await executeQuery(
|
|
324
|
+
`SELECT id, created_at
|
|
325
|
+
FROM ${recoveryTable}
|
|
326
|
+
WHERE google_sub = ?
|
|
327
|
+
AND created_at >= (UTC_TIMESTAMP() - INTERVAL ${resendCooldownSeconds} SECOND)
|
|
328
|
+
ORDER BY id DESC
|
|
329
|
+
LIMIT 1`,
|
|
330
|
+
[normalizedGoogleSub],
|
|
331
|
+
connection,
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
const row = Array.isArray(rows) ? rows[0] : null;
|
|
335
|
+
if (!row) return null;
|
|
336
|
+
const createdAtMs = row.created_at ? Date.parse(row.created_at) : NaN;
|
|
337
|
+
const elapsedSeconds = Number.isFinite(createdAtMs) ? Math.max(0, Math.floor((Date.now() - createdAtMs) / 1000)) : resendCooldownSeconds;
|
|
338
|
+
const retryAfterSeconds = Math.max(0, resendCooldownSeconds - elapsedSeconds);
|
|
339
|
+
return {
|
|
340
|
+
id: Number(row.id || 0),
|
|
341
|
+
created_at: row.created_at ? new Date(row.created_at).toISOString() : null,
|
|
342
|
+
retry_after_seconds: retryAfterSeconds,
|
|
343
|
+
};
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const revokeActiveCodesForGoogleSub = async (googleSub, connection = null) => {
|
|
347
|
+
const normalizedGoogleSub = normalizeGoogleSubject(googleSub);
|
|
348
|
+
if (!normalizedGoogleSub) return 0;
|
|
349
|
+
|
|
350
|
+
const result = await executeQuery(
|
|
351
|
+
`UPDATE ${recoveryTable}
|
|
352
|
+
SET revoked_at = COALESCE(revoked_at, UTC_TIMESTAMP()),
|
|
353
|
+
updated_at = UTC_TIMESTAMP()
|
|
354
|
+
WHERE google_sub = ?
|
|
355
|
+
AND revoked_at IS NULL
|
|
356
|
+
AND consumed_at IS NULL
|
|
357
|
+
AND expires_at > UTC_TIMESTAMP()`,
|
|
358
|
+
[normalizedGoogleSub],
|
|
359
|
+
connection,
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
return Number(result?.affectedRows || 0);
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const requestPasswordRecoveryCode = async ({ googleSub = '', email = '', ownerJid = '', purpose = 'reset', requestMeta = {} } = {}, connection = null) => {
|
|
366
|
+
const identity = normalizeIdentity({ googleSub, email, ownerJid });
|
|
367
|
+
if (!identity.hasIdentity) {
|
|
368
|
+
throw buildHttpError('Informe google_sub, email ou owner_jid.', {
|
|
369
|
+
statusCode: 400,
|
|
370
|
+
code: 'IDENTITY_REQUIRED',
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const knownUser = await userPasswordAuthService.findKnownGoogleUserByIdentity(identity, connection);
|
|
375
|
+
if (!knownUser?.google_sub) {
|
|
376
|
+
return {
|
|
377
|
+
accepted: true,
|
|
378
|
+
queued: false,
|
|
379
|
+
expires_in_seconds: ttlSeconds,
|
|
380
|
+
masked_email: null,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const recipientEmail = normalizeEmail(knownUser.email || identity.email);
|
|
385
|
+
if (!recipientEmail || !recipientEmail.includes('@')) {
|
|
386
|
+
return {
|
|
387
|
+
accepted: true,
|
|
388
|
+
queued: false,
|
|
389
|
+
expires_in_seconds: ttlSeconds,
|
|
390
|
+
masked_email: null,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (typeof queueAutomatedEmail !== 'function') {
|
|
395
|
+
throw buildHttpError('Servico de e-mail indisponivel para recuperacao de senha.', {
|
|
396
|
+
statusCode: 503,
|
|
397
|
+
code: 'EMAIL_AUTOMATION_UNAVAILABLE',
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const requestsInLastDay = await countDailyRequestsByGoogleSub(knownUser.google_sub, connection);
|
|
402
|
+
if (requestsInLastDay >= dailyRequestLimit) {
|
|
403
|
+
const retryAfterSeconds = await getRetryAfterForWindowByGoogleSub(knownUser.google_sub, 24 * 60 * 60, connection).catch(() => 24 * 60 * 60);
|
|
404
|
+
throw buildHttpError('Limite diario de solicitacoes atingido. Tente novamente amanha.', {
|
|
405
|
+
statusCode: 429,
|
|
406
|
+
code: 'PASSWORD_RECOVERY_DAILY_LIMIT',
|
|
407
|
+
details: {
|
|
408
|
+
retry_after_seconds: retryAfterSeconds,
|
|
409
|
+
limit: dailyRequestLimit,
|
|
410
|
+
window_seconds: 24 * 60 * 60,
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const requestsInLastHour = await countHourlyRequestsByGoogleSub(knownUser.google_sub, connection);
|
|
416
|
+
if (requestsInLastHour >= hourlyRequestLimit) {
|
|
417
|
+
const retryAfterSeconds = await getRetryAfterForWindowByGoogleSub(knownUser.google_sub, 60 * 60, connection).catch(() => 60 * 60);
|
|
418
|
+
throw buildHttpError('Muitas solicitacoes em pouco tempo. Aguarde alguns minutos para tentar novamente.', {
|
|
419
|
+
statusCode: 429,
|
|
420
|
+
code: 'PASSWORD_RECOVERY_HOURLY_LIMIT',
|
|
421
|
+
details: {
|
|
422
|
+
retry_after_seconds: retryAfterSeconds,
|
|
423
|
+
limit: hourlyRequestLimit,
|
|
424
|
+
window_seconds: 60 * 60,
|
|
425
|
+
},
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const recentRequest = await getRecentRequestWithinCooldown(knownUser.google_sub, connection);
|
|
430
|
+
if (recentRequest?.id) {
|
|
431
|
+
const retryAfterSeconds = Math.max(1, Number(recentRequest.retry_after_seconds || resendCooldownSeconds));
|
|
432
|
+
throw buildHttpError(`Aguarde ${retryAfterSeconds}s antes de solicitar um novo codigo.`, {
|
|
433
|
+
statusCode: 429,
|
|
434
|
+
code: 'PASSWORD_RECOVERY_COOLDOWN_ACTIVE',
|
|
435
|
+
details: {
|
|
436
|
+
retry_after_seconds: retryAfterSeconds,
|
|
437
|
+
cooldown_seconds: resendCooldownSeconds,
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const verificationCode = generateSixDigitCode();
|
|
443
|
+
const normalizedPurpose = normalizePurpose(purpose);
|
|
444
|
+
const codeHash = await buildCodeHashV3({
|
|
445
|
+
code: verificationCode,
|
|
446
|
+
googleSub: knownUser.google_sub,
|
|
447
|
+
purpose: normalizedPurpose,
|
|
448
|
+
});
|
|
449
|
+
const recipientEmailHash = buildSensitiveMetadataHash(recipientEmail, { scope: 'email' });
|
|
450
|
+
const requestIpHash = buildSensitiveMetadataHash(normalizeIp(requestMeta?.remoteIp), {
|
|
451
|
+
scope: 'ip',
|
|
452
|
+
});
|
|
453
|
+
const requestUserAgentHash = buildSensitiveMetadataHash(normalizeUserAgent(requestMeta?.userAgent), { scope: 'user_agent' });
|
|
454
|
+
|
|
455
|
+
await revokeActiveCodesForGoogleSub(knownUser.google_sub, connection);
|
|
456
|
+
|
|
457
|
+
await executeQuery(
|
|
458
|
+
`INSERT INTO ${recoveryTable}
|
|
459
|
+
(
|
|
460
|
+
google_sub,
|
|
461
|
+
email,
|
|
462
|
+
email_hash,
|
|
463
|
+
owner_jid,
|
|
464
|
+
purpose,
|
|
465
|
+
code_hash,
|
|
466
|
+
attempts,
|
|
467
|
+
max_attempts,
|
|
468
|
+
requested_ip,
|
|
469
|
+
requested_ip_hash,
|
|
470
|
+
requested_user_agent,
|
|
471
|
+
requested_user_agent_hash,
|
|
472
|
+
expires_at
|
|
473
|
+
)
|
|
474
|
+
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, UTC_TIMESTAMP() + INTERVAL ${ttlSeconds} SECOND)`,
|
|
475
|
+
[knownUser.google_sub, '', recipientEmailHash, normalizeJid(knownUser.owner_jid || identity.ownerJid) || null, normalizedPurpose, codeHash, maxAttempts, null, requestIpHash, null, requestUserAgentHash],
|
|
476
|
+
connection,
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
try {
|
|
480
|
+
await queueAutomatedEmail({
|
|
481
|
+
to: recipientEmail,
|
|
482
|
+
name: knownUser.name || '',
|
|
483
|
+
templateKey: 'password_reset_code',
|
|
484
|
+
templateData: {
|
|
485
|
+
name: knownUser.name || '',
|
|
486
|
+
code: verificationCode,
|
|
487
|
+
email: recipientEmail,
|
|
488
|
+
purpose: normalizedPurpose,
|
|
489
|
+
expiresInMinutes: Math.max(1, Math.ceil(ttlSeconds / 60)),
|
|
490
|
+
},
|
|
491
|
+
metadata: {
|
|
492
|
+
trigger: 'web_user_password_recovery_code',
|
|
493
|
+
purpose: normalizedPurpose,
|
|
494
|
+
google_sub: knownUser.google_sub,
|
|
495
|
+
owner_jid: normalizeJid(knownUser.owner_jid || '') || null,
|
|
496
|
+
remote_ip: normalizeIp(requestMeta?.remoteIp) || null,
|
|
497
|
+
},
|
|
498
|
+
priority: 95,
|
|
499
|
+
idempotencyKey: `web_user_password_recovery:${knownUser.google_sub}:${normalizedPurpose}:${new Date().toISOString().slice(0, 16)}`,
|
|
500
|
+
});
|
|
501
|
+
} catch (error) {
|
|
502
|
+
await executeQuery(
|
|
503
|
+
`UPDATE ${recoveryTable}
|
|
504
|
+
SET revoked_at = COALESCE(revoked_at, UTC_TIMESTAMP()),
|
|
505
|
+
updated_at = UTC_TIMESTAMP()
|
|
506
|
+
WHERE google_sub = ?
|
|
507
|
+
AND code_hash = ?
|
|
508
|
+
AND consumed_at IS NULL
|
|
509
|
+
AND revoked_at IS NULL`,
|
|
510
|
+
[knownUser.google_sub, codeHash],
|
|
511
|
+
connection,
|
|
512
|
+
).catch(() => {});
|
|
513
|
+
|
|
514
|
+
if (logger && typeof logger.warn === 'function') {
|
|
515
|
+
logger.warn('Falha ao enfileirar e-mail de recuperacao de senha.', {
|
|
516
|
+
action: 'web_user_password_recovery_email_enqueue_failed',
|
|
517
|
+
google_sub: knownUser.google_sub,
|
|
518
|
+
email: recipientEmail,
|
|
519
|
+
error: error?.message,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
throw buildHttpError('Falha ao enviar codigo de verificacao por e-mail.', {
|
|
524
|
+
statusCode: 502,
|
|
525
|
+
code: 'RECOVERY_EMAIL_SEND_FAILED',
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return {
|
|
530
|
+
accepted: true,
|
|
531
|
+
queued: true,
|
|
532
|
+
expires_in_seconds: ttlSeconds,
|
|
533
|
+
masked_email: maskEmail(recipientEmail),
|
|
534
|
+
};
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
const verifyPasswordRecoveryCode = async ({ googleSub = '', email = '', ownerJid = '', purpose = '', code = '', password = '', requestMeta = {} } = {}) => {
|
|
538
|
+
const identity = normalizeIdentity({ googleSub, email, ownerJid });
|
|
539
|
+
if (!identity.hasIdentity) {
|
|
540
|
+
throw buildHttpError('Informe google_sub, email ou owner_jid.', {
|
|
541
|
+
statusCode: 400,
|
|
542
|
+
code: 'IDENTITY_REQUIRED',
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const normalizedCode = normalizeCode(code);
|
|
547
|
+
if (!/^\d{6}$/.test(normalizedCode)) {
|
|
548
|
+
throw buildHttpError('Codigo de verificacao invalido.', {
|
|
549
|
+
statusCode: 400,
|
|
550
|
+
code: 'INVALID_VERIFICATION_CODE',
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const passwordValidation = userPasswordAuthService.validatePassword(password);
|
|
555
|
+
if (!passwordValidation?.valid) {
|
|
556
|
+
throw buildHttpError(passwordValidation?.errors?.[0]?.message || 'Senha invalida.', {
|
|
557
|
+
statusCode: 400,
|
|
558
|
+
code: 'INVALID_PASSWORD',
|
|
559
|
+
details: Array.isArray(passwordValidation?.errors) ? passwordValidation.errors : undefined,
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const knownUser = await userPasswordAuthService.findKnownGoogleUserByIdentity(identity);
|
|
564
|
+
if (!knownUser?.google_sub) {
|
|
565
|
+
throw buildHttpError('Codigo de verificacao invalido ou expirado.', {
|
|
566
|
+
statusCode: 401,
|
|
567
|
+
code: 'INVALID_VERIFICATION_CODE',
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const normalizedPurpose = String(purpose || '').trim() ? normalizePurpose(purpose) : '';
|
|
572
|
+
const activeCode = await findLatestActiveCodeByIdentity(
|
|
573
|
+
{
|
|
574
|
+
googleSub: knownUser.google_sub,
|
|
575
|
+
email: knownUser.email || identity.email,
|
|
576
|
+
ownerJid: knownUser.owner_jid || identity.ownerJid,
|
|
577
|
+
purpose: normalizedPurpose,
|
|
578
|
+
},
|
|
579
|
+
null,
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
if (!activeCode?.id || !activeCode.google_sub) {
|
|
583
|
+
throw buildHttpError('Codigo de verificacao invalido ou expirado.', {
|
|
584
|
+
statusCode: 401,
|
|
585
|
+
code: 'INVALID_VERIFICATION_CODE',
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const expectedHashV3 = await buildCodeHashV3({
|
|
590
|
+
code: normalizedCode,
|
|
591
|
+
googleSub: activeCode.google_sub,
|
|
592
|
+
purpose: activeCode.purpose,
|
|
593
|
+
});
|
|
594
|
+
let isCodeValid = secureHexEquals(expectedHashV3, activeCode.code_hash);
|
|
595
|
+
|
|
596
|
+
if (!isCodeValid && activeCode.email) {
|
|
597
|
+
const expectedHashV2 = await buildCodeHashV2({
|
|
598
|
+
code: normalizedCode,
|
|
599
|
+
googleSub: activeCode.google_sub,
|
|
600
|
+
email: activeCode.email,
|
|
601
|
+
purpose: activeCode.purpose,
|
|
602
|
+
});
|
|
603
|
+
isCodeValid = secureHexEquals(expectedHashV2, activeCode.code_hash);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (!isCodeValid) {
|
|
607
|
+
await executeQuery(
|
|
608
|
+
`UPDATE ${recoveryTable}
|
|
609
|
+
SET attempts = attempts + 1,
|
|
610
|
+
last_attempt_at = UTC_TIMESTAMP(),
|
|
611
|
+
revoked_at = IF(attempts + 1 >= max_attempts, COALESCE(revoked_at, UTC_TIMESTAMP()), revoked_at),
|
|
612
|
+
updated_at = UTC_TIMESTAMP()
|
|
613
|
+
WHERE id = ?`,
|
|
614
|
+
[activeCode.id],
|
|
615
|
+
).catch(() => {});
|
|
616
|
+
|
|
617
|
+
throw buildHttpError('Codigo de verificacao invalido ou expirado.', {
|
|
618
|
+
statusCode: 401,
|
|
619
|
+
code: 'INVALID_VERIFICATION_CODE',
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const credential = await withTransaction(async (connection) => {
|
|
624
|
+
const consumeResult = await executeQuery(
|
|
625
|
+
`UPDATE ${recoveryTable}
|
|
626
|
+
SET consumed_at = UTC_TIMESTAMP(),
|
|
627
|
+
last_attempt_at = UTC_TIMESTAMP(),
|
|
628
|
+
updated_at = UTC_TIMESTAMP()
|
|
629
|
+
WHERE id = ?
|
|
630
|
+
AND consumed_at IS NULL
|
|
631
|
+
AND revoked_at IS NULL
|
|
632
|
+
AND expires_at > UTC_TIMESTAMP()`,
|
|
633
|
+
[activeCode.id],
|
|
634
|
+
connection,
|
|
635
|
+
);
|
|
636
|
+
|
|
637
|
+
if (Number(consumeResult?.affectedRows || 0) <= 0) {
|
|
638
|
+
throw buildHttpError('Codigo de verificacao invalido ou expirado.', {
|
|
639
|
+
statusCode: 401,
|
|
640
|
+
code: 'INVALID_VERIFICATION_CODE',
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const updatedCredential = await userPasswordAuthService.setPasswordForIdentity(
|
|
645
|
+
{
|
|
646
|
+
googleSub: activeCode.google_sub,
|
|
647
|
+
email: knownUser.email || activeCode.email,
|
|
648
|
+
ownerJid: activeCode.owner_jid || knownUser.owner_jid,
|
|
649
|
+
password,
|
|
650
|
+
},
|
|
651
|
+
connection,
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
await userPasswordAuthService
|
|
655
|
+
.clearFailuresForIdentity(
|
|
656
|
+
{
|
|
657
|
+
googleSub: activeCode.google_sub,
|
|
658
|
+
},
|
|
659
|
+
connection,
|
|
660
|
+
)
|
|
661
|
+
.catch(() => null);
|
|
662
|
+
|
|
663
|
+
await executeQuery(
|
|
664
|
+
`UPDATE ${recoveryTable}
|
|
665
|
+
SET requested_ip = NULL,
|
|
666
|
+
requested_ip_hash = COALESCE(?, requested_ip_hash),
|
|
667
|
+
requested_user_agent = NULL,
|
|
668
|
+
requested_user_agent_hash = COALESCE(?, requested_user_agent_hash),
|
|
669
|
+
updated_at = UTC_TIMESTAMP()
|
|
670
|
+
WHERE id = ?`,
|
|
671
|
+
[
|
|
672
|
+
buildSensitiveMetadataHash(normalizeIp(requestMeta?.remoteIp), {
|
|
673
|
+
scope: 'ip',
|
|
674
|
+
}),
|
|
675
|
+
buildSensitiveMetadataHash(normalizeUserAgent(requestMeta?.userAgent), {
|
|
676
|
+
scope: 'user_agent',
|
|
677
|
+
}),
|
|
678
|
+
activeCode.id,
|
|
679
|
+
],
|
|
680
|
+
connection,
|
|
681
|
+
).catch(() => null);
|
|
682
|
+
|
|
683
|
+
return updatedCredential;
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
if (credential?.google_sub && typeof revokeWebSessionsByIdentity === 'function') {
|
|
687
|
+
try {
|
|
688
|
+
await revokeWebSessionsByIdentity({
|
|
689
|
+
googleSub: credential.google_sub,
|
|
690
|
+
email: credential.email || knownUser.email || activeCode.email,
|
|
691
|
+
ownerJid: credential.owner_jid || activeCode.owner_jid || knownUser.owner_jid,
|
|
692
|
+
});
|
|
693
|
+
} catch (error) {
|
|
694
|
+
if (logger && typeof logger.warn === 'function') {
|
|
695
|
+
logger.warn('Senha redefinida, mas falhou ao revogar sessoes web ativas.', {
|
|
696
|
+
action: 'web_user_password_recovery_session_revoke_failed',
|
|
697
|
+
google_sub: credential.google_sub,
|
|
698
|
+
error: error?.message,
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
throw buildHttpError('Senha atualizada, mas nao foi possivel revogar sessoes ativas.', {
|
|
702
|
+
statusCode: 503,
|
|
703
|
+
code: 'SESSION_REVOKE_FAILED',
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
return {
|
|
709
|
+
updated: true,
|
|
710
|
+
credential,
|
|
711
|
+
purpose: activeCode.purpose,
|
|
712
|
+
masked_email: maskEmail(knownUser.email || activeCode.email),
|
|
713
|
+
};
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
return {
|
|
717
|
+
getPolicy: () => ({
|
|
718
|
+
code_ttl_seconds: ttlSeconds,
|
|
719
|
+
max_attempts: maxAttempts,
|
|
720
|
+
resend_cooldown_seconds: resendCooldownSeconds,
|
|
721
|
+
hourly_request_limit: hourlyRequestLimit,
|
|
722
|
+
daily_request_limit: dailyRequestLimit,
|
|
723
|
+
}),
|
|
724
|
+
requestPasswordRecoveryCode,
|
|
725
|
+
verifyPasswordRecoveryCode,
|
|
726
|
+
findLatestActiveCodeByIdentity,
|
|
727
|
+
};
|
|
728
|
+
};
|