@omnizap-system/omnizap 2.6.1 → 2.6.3

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 (172) hide show
  1. package/.env.example +78 -9
  2. package/.github/workflows/ci.yml +3 -3
  3. package/.github/workflows/security-runner-hardening.yml +1 -1
  4. package/.github/workflows/security-zap-full-scan.yml +1 -0
  5. package/app/config/index.js +6 -0
  6. package/app/configParts/adminIdentity.js +36 -7
  7. package/app/configParts/baileysConfig.js +343 -56
  8. package/app/configParts/groupUtils.js +226 -0
  9. package/app/configParts/loggerConfig.js +185 -0
  10. package/app/configParts/messagePersistenceService.js +307 -5
  11. package/app/configParts/sessionConfig.js +242 -0
  12. package/app/connection/baileysCompatibility.test.js +10 -1
  13. package/app/connection/baileysDbAuthState.js +205 -9
  14. package/app/connection/baileysLibsignalPatch.js +210 -0
  15. package/app/connection/groupOwnerWriteStateResolver.js +141 -0
  16. package/app/connection/socketController.js +694 -123
  17. package/app/connection/socketController.multiSession.test.js +128 -0
  18. package/app/controllers/messageController.js +1 -1
  19. package/app/controllers/messagePipeline/commandMiddleware.js +12 -10
  20. package/app/controllers/messagePipeline/conversationMiddleware.js +2 -1
  21. package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +104 -0
  22. package/app/controllers/messagePipeline/preProcessingMiddlewares.js +96 -4
  23. package/app/controllers/messageProcessingPipeline.js +90 -9
  24. package/app/controllers/messageProcessingPipeline.test.js +202 -0
  25. package/app/modules/adminModule/AGENT.md +1 -1
  26. package/app/modules/adminModule/commandConfig.json +3318 -1347
  27. package/app/modules/adminModule/groupCommandHandlers.js +856 -14
  28. package/app/modules/adminModule/groupCommandHandlers.test.js +375 -9
  29. package/app/modules/adminModule/groupWarningRepository.js +152 -0
  30. package/app/modules/aiModule/AGENT.md +47 -30
  31. package/app/modules/aiModule/aiConfigRuntime.js +1 -0
  32. package/app/modules/aiModule/catCommand.js +132 -25
  33. package/app/modules/aiModule/commandConfig.json +114 -28
  34. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +54 -6
  35. package/app/modules/gameModule/AGENT.md +1 -1
  36. package/app/modules/gameModule/commandConfig.json +29 -0
  37. package/app/modules/menuModule/AGENT.md +1 -1
  38. package/app/modules/menuModule/commandConfig.json +45 -10
  39. package/app/modules/menuModule/menuCatalogService.js +190 -0
  40. package/app/modules/menuModule/menuCommandUsageRepository.js +109 -0
  41. package/app/modules/menuModule/menuDynamicService.js +511 -0
  42. package/app/modules/menuModule/menuDynamicService.test.js +141 -0
  43. package/app/modules/menuModule/menus.js +36 -5
  44. package/app/modules/playModule/AGENT.md +10 -5
  45. package/app/modules/playModule/commandConfig.json +74 -16
  46. package/app/modules/playModule/playCommandConstants.js +13 -7
  47. package/app/modules/playModule/playCommandCore.js +4 -6
  48. package/app/modules/playModule/{playCommandYtDlpClient.js → playCommandMediaClient.js} +684 -332
  49. package/app/modules/playModule/playConfigRuntime.js +5 -6
  50. package/app/modules/playModule/playModuleCriticalFlows.test.js +44 -59
  51. package/app/modules/quoteModule/AGENT.md +1 -1
  52. package/app/modules/quoteModule/commandConfig.json +29 -0
  53. package/app/modules/rpgPokemonModule/AGENT.md +1 -1
  54. package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
  55. package/app/modules/statsModule/AGENT.md +1 -1
  56. package/app/modules/statsModule/commandConfig.json +58 -0
  57. package/app/modules/stickerModule/AGENT.md +1 -1
  58. package/app/modules/stickerModule/commandConfig.json +145 -0
  59. package/app/modules/stickerPackModule/AGENT.md +1 -1
  60. package/app/modules/stickerPackModule/autoPackCollectorService.js +5 -1
  61. package/app/modules/stickerPackModule/commandConfig.json +29 -0
  62. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +1 -1
  63. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +78 -57
  64. package/app/modules/stickerPackModule/stickerPackService.js +13 -6
  65. package/app/modules/systemMetricsModule/AGENT.md +1 -1
  66. package/app/modules/systemMetricsModule/commandConfig.json +29 -0
  67. package/app/modules/tiktokModule/AGENT.md +1 -1
  68. package/app/modules/tiktokModule/commandConfig.json +29 -0
  69. package/app/modules/userModule/AGENT.md +1 -1
  70. package/app/modules/userModule/commandConfig.json +29 -0
  71. package/app/modules/waifuPicsModule/AGENT.md +57 -27
  72. package/app/modules/waifuPicsModule/commandConfig.json +87 -0
  73. package/app/observability/metrics.js +136 -0
  74. package/app/services/ai/commandConfigEnrichmentService.js +229 -47
  75. package/app/services/ai/geminiService.js +131 -7
  76. package/app/services/ai/geminiService.test.js +59 -2
  77. package/app/services/ai/moduleAiHelpCoreService.js +33 -4
  78. package/app/services/group/groupMetadataService.js +24 -1
  79. package/app/services/infra/dbWriteQueue.js +51 -21
  80. package/app/services/messaging/newsBroadcastService.js +843 -27
  81. package/app/services/multiSession/assignmentBalancerService.js +452 -0
  82. package/app/services/multiSession/groupOwnershipRepository.js +346 -0
  83. package/app/services/multiSession/groupOwnershipService.js +809 -0
  84. package/app/services/multiSession/groupOwnershipService.test.js +317 -0
  85. package/app/services/multiSession/sessionRegistryService.js +239 -0
  86. package/app/store/aiPromptStore.js +36 -19
  87. package/app/store/groupConfigStore.js +41 -5
  88. package/app/store/premiumUserStore.js +21 -7
  89. package/app/utils/antiLink/antiLinkModule.js +391 -25
  90. package/app/workers/aiHelperContinuousLearningWorker.js +512 -0
  91. package/database/index.js +6 -0
  92. package/database/migrations/20260307_d0_hardening_down.sql +1 -1
  93. package/database/migrations/20260314_d7_canonical_sender_down.sql +1 -1
  94. package/database/migrations/20260406_d30_security_analytics_down.sql +1 -1
  95. package/database/migrations/20260411_d35_group_community_metadata_down.sql +59 -0
  96. package/database/migrations/20260411_d35_group_community_metadata_up.sql +62 -0
  97. package/database/migrations/20260412_d36_system_config_tables_down.sql +32 -0
  98. package/database/migrations/20260412_d36_system_config_tables_up.sql +66 -0
  99. package/database/migrations/20260413_d37_group_user_warnings_down.sql +11 -0
  100. package/database/migrations/20260413_d37_group_user_warnings_up.sql +24 -0
  101. package/database/migrations/20260414_d38_multi_session_foundation_down.sql +72 -0
  102. package/database/migrations/20260414_d38_multi_session_foundation_up.sql +125 -0
  103. package/database/migrations/20260414_d39_multi_session_cutover_down.sql +103 -0
  104. package/database/migrations/20260414_d39_multi_session_cutover_up.sql +83 -0
  105. package/database/schema.sql +102 -1
  106. package/docker-compose.yml +4 -1
  107. package/docs/compliance/acceptable-use-policy-2026-03-07.md +1 -1
  108. package/docs/compliance/privacy-policy-2026-03-07.md +2 -2
  109. package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
  110. package/docs/security/network-hardening-runbook-2026-03-07.md +53 -0
  111. package/docs/security/omnizap-static-security-headers.conf +25 -0
  112. package/ecosystem.prod.config.cjs +31 -11
  113. package/index.js +52 -18
  114. package/observability/alert-rules.yml +20 -0
  115. package/observability/grafana/dashboards/omnizap-system-admin.json +229 -0
  116. package/observability/mysql-setup.sql +4 -4
  117. package/observability/system-admin-observability.md +26 -0
  118. package/package.json +14 -6
  119. package/public/comandos/commands-catalog.json +2253 -78
  120. package/public/css/payments-react.css +478 -0
  121. package/public/js/apps/commandsReactApp.js +267 -87
  122. package/public/js/apps/createPackApp.js +3 -3
  123. package/public/js/apps/homeReactApp.js +2 -2
  124. package/public/js/apps/paymentsCancelReactApp.js +45 -0
  125. package/public/js/apps/paymentsReactApp.js +399 -0
  126. package/public/js/apps/paymentsSuccessReactApp.js +148 -0
  127. package/public/js/apps/stickersApp.js +255 -103
  128. package/public/js/apps/termsReactApp.js +57 -8
  129. package/public/js/apps/userPasswordResetReactApp.js +406 -0
  130. package/public/js/apps/userReactApp.js +96 -47
  131. package/public/js/apps/userSystemAdmReactApp.js +1506 -0
  132. package/public/pages/pagamentos-cancelado.html +21 -0
  133. package/public/pages/pagamentos-sucesso.html +21 -0
  134. package/public/pages/pagamentos.html +30 -0
  135. package/public/pages/politica-de-privacidade.html +1 -1
  136. package/public/pages/stickers.html +5 -5
  137. package/public/pages/termos-de-uso-texto-integral.html +1 -1
  138. package/public/pages/termos-de-uso.html +1 -1
  139. package/public/pages/user-password-reset.html +3 -4
  140. package/public/pages/user-systemadm.html +8 -462
  141. package/public/pages/user.html +1 -1
  142. package/scripts/clear-whatsapp-session.sh +123 -0
  143. package/scripts/core-ai-mode.mjs +163 -0
  144. package/scripts/deploy.sh +13 -0
  145. package/scripts/enrich-command-config-ux-openai.mjs +492 -0
  146. package/scripts/generate-commands-catalog.mjs +155 -0
  147. package/scripts/new-whatsapp-session.sh +564 -0
  148. package/scripts/security-web-surface-check.mjs +218 -0
  149. package/server/controllers/admin/adminPanelHandlers.js +253 -3
  150. package/server/controllers/admin/systemAdminController.js +254 -0
  151. package/server/controllers/payments/paymentsController.js +731 -0
  152. package/server/controllers/sticker/stickerCatalogController.js +9 -23
  153. package/server/controllers/system/contactController.js +9 -17
  154. package/server/controllers/system/stickerCatalogSystemContext.js +27 -6
  155. package/server/controllers/system/systemController.js +228 -1
  156. package/server/controllers/userController.js +6 -0
  157. package/server/email/emailAutomationRuntime.js +36 -1
  158. package/server/email/emailAutomationService.js +42 -1
  159. package/server/email/emailTemplateService.js +140 -33
  160. package/server/http/httpRequestUtils.js +18 -14
  161. package/server/http/httpServer.js +8 -4
  162. package/server/middleware/securityHeaders.js +35 -3
  163. package/server/routes/admin/systemAdminRouter.js +6 -0
  164. package/server/routes/indexRouter.js +50 -6
  165. package/server/routes/observability/grafanaProxyRouter.js +254 -0
  166. package/server/routes/payments/paymentsRouter.js +47 -0
  167. package/server/routes/static/staticPageRouter.js +30 -1
  168. package/server/utils/publicContact.js +31 -0
  169. package/utils/whatsapp/contactEnv.js +39 -0
  170. package/vite.config.mjs +5 -1
  171. package/app/modules/playModule/local/installYtDlp.js +0 -25
  172. package/app/modules/playModule/local/ytDlpInstaller.js +0 -28
@@ -1,6 +1,9 @@
1
1
  import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
2
+ import logger from '#logger';
3
+ import { resolveAdminPhoneFromEnv, resolveBotPhoneFromEnv, resolveSupportPhoneFromEnv } from '../../utils/whatsapp/contactEnv.js';
2
4
  const DEFAULT_SITE_ORIGIN = 'https://omnizap.shop';
3
5
  const DEFAULT_BRAND_NAME = 'OmniZap';
6
+ const DEFAULT_BRAND_LOGO_PATH = '/assets/images/brand-logo-128.webp';
4
7
 
5
8
  const resolveSiteOrigin = () =>
6
9
  String(process.env.SITE_ORIGIN || process.env.WHATSAPP_LOGIN_BASE_URL || DEFAULT_SITE_ORIGIN)
@@ -27,6 +30,24 @@ const escapeHtml = (value) =>
27
30
  .replace(/"/g, '"')
28
31
  .replace(/'/g, ''');
29
32
 
33
+ const summarizePayloadKeys = (value, maxItems = 24) => {
34
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return [];
35
+ return Object.keys(value)
36
+ .map((key) =>
37
+ String(key || '')
38
+ .trim()
39
+ .slice(0, 64),
40
+ )
41
+ .filter(Boolean)
42
+ .slice(0, maxItems);
43
+ };
44
+
45
+ const clipTemplatePreview = (value, maxLength = 140) =>
46
+ String(value || '')
47
+ .replace(/\s+/g, ' ')
48
+ .trim()
49
+ .slice(0, maxLength);
50
+
30
51
  const normalizeHttpUrl = (value, fallback = '') => {
31
52
  const normalized = String(value || '')
32
53
  .trim()
@@ -47,6 +68,19 @@ const normalizeEmailAddress = (value) => {
47
68
  return candidate.slice(0, 255);
48
69
  };
49
70
 
71
+ const resolveBrandLogoUrl = ({ siteOrigin = '', payloadLogoUrl = '' } = {}) => {
72
+ const fallbackLogoUrl = normalizeHttpUrl(`${String(siteOrigin || DEFAULT_SITE_ORIGIN).replace(/\/+$/, '')}${DEFAULT_BRAND_LOGO_PATH}`, `${DEFAULT_SITE_ORIGIN}${DEFAULT_BRAND_LOGO_PATH}`);
73
+
74
+ const explicitLogoUrl = normalizeHttpUrl(payloadLogoUrl || process.env.EMAIL_BRAND_LOGO_URL || '', '');
75
+ if (!explicitLogoUrl) return fallbackLogoUrl;
76
+
77
+ const explicitLower = explicitLogoUrl.toLowerCase();
78
+ const looksLikeFavicon = explicitLower.endsWith('/favicon.ico') || explicitLower.includes('/favicon-');
79
+ if (looksLikeFavicon) return fallbackLogoUrl;
80
+
81
+ return explicitLogoUrl;
82
+ };
83
+
50
84
  const normalizePhoneDigits = (value, maxLength = 20) =>
51
85
  String(value || '')
52
86
  .replace(/\D+/g, '')
@@ -77,7 +111,7 @@ const resolveBrandConfig = (payload = {}) => {
77
111
  const supportFallback = `${siteOrigin}/termos-de-uso/`;
78
112
  const replyToAddress = normalizeEmailAddress(payload?.replyTo || process.env.SMTP_REPLY_TO || process.env.EMAIL_REPLY_TO || process.env.MAIL_REPLY_TO || '');
79
113
  const fromAddress = normalizeEmailAddress(process.env.SMTP_FROM || process.env.EMAIL_FROM || process.env.MAIL_FROM || process.env.SMTP_USER || process.env.EMAIL_USER || process.env.MAIL_USER || '');
80
- const supportPhoneCandidate = normalizePhoneDigits(payload?.supportPhone || process.env.EMAIL_BRAND_SUPPORT_PHONE || process.env.WHATSAPP_SUPPORT_NUMBER || process.env.OWNER_NUMBER || '', 20);
114
+ const supportPhoneCandidate = normalizePhoneDigits(payload?.supportPhone || resolveSupportPhoneFromEnv({ fallback: resolveAdminPhoneFromEnv({ fallback: '' }) }) || '', 20);
81
115
  const supportPhoneDigits = isLikelyPhoneDigits(supportPhoneCandidate) ? supportPhoneCandidate : '';
82
116
  const supportPhonePn = formatPhonePn(supportPhoneDigits);
83
117
  const supportWhatsappUrl = supportPhoneDigits ? `https://wa.me/${supportPhoneDigits}` : '';
@@ -88,7 +122,10 @@ const resolveBrandConfig = (payload = {}) => {
88
122
  siteOrigin,
89
123
  brandName: normalizeText(payload?.brandName || process.env.EMAIL_BRAND_NAME || DEFAULT_BRAND_NAME, 80) || DEFAULT_BRAND_NAME,
90
124
  brandTagline: normalizeText(payload?.brandTagline || process.env.EMAIL_BRAND_TAGLINE || 'Automação profissional para WhatsApp.', 120) || null,
91
- brandLogoUrl: normalizeHttpUrl(payload?.brandLogoUrl || payload?.logoUrl || process.env.EMAIL_BRAND_LOGO_URL || '', ''),
125
+ brandLogoUrl: resolveBrandLogoUrl({
126
+ siteOrigin,
127
+ payloadLogoUrl: payload?.brandLogoUrl || payload?.logoUrl || '',
128
+ }),
92
129
  supportUrl: resolvedSupportUrl,
93
130
  supportLabel: resolvedSupportLabel || 'Central de suporte',
94
131
  supportEmail: replyToAddress || fromAddress || '',
@@ -127,53 +164,75 @@ const renderEmailLayout = ({ payload = {}, preheader = '', heading = '', greetin
127
164
  const safeSecurityNote = normalizeText(securityNote, 220);
128
165
  const safeFooterMessage = normalizeText(footerMessage, 220);
129
166
  const year = __timeNow().getUTCFullYear();
167
+ const generatedAt = __timeNowIso();
130
168
 
131
- const logoBlock = brand.brandLogoUrl ? `<img src="${escapeHtml(brand.brandLogoUrl)}" alt="${escapeHtml(brand.brandName)}" width="132" style="display:block;border:0;outline:none;text-decoration:none;height:auto;margin:0 auto;" />` : `<div style="display:inline-block;font-size:26px;font-weight:800;color:#0f172a;letter-spacing:0.2px;">${escapeHtml(brand.brandName)}</div>`;
169
+ const logoBlock = brand.brandLogoUrl ? `<img src="${escapeHtml(brand.brandLogoUrl)}" alt="${escapeHtml(brand.brandName)}" width="138" style="display:block;border:0;outline:none;text-decoration:none;height:auto;" />` : `<div style="display:inline-block;font-size:24px;font-weight:800;line-height:1.1;color:#ffffff;letter-spacing:0.3px;">${escapeHtml(brand.brandName)}</div>`;
132
170
 
133
- const greetingBlock = safeGreeting ? `<p style="margin:0 0 10px;color:#0f172a;font-size:16px;font-weight:700;line-height:1.5;">${escapeHtml(safeGreeting)}</p>` : '';
134
- const introBlock = safeIntro ? `<p style="margin:0 0 14px;color:#334155;font-size:15px;line-height:1.65;">${escapeHtml(safeIntro)}</p>` : '';
171
+ const headingBlock = safeHeading ? `<h1 style="margin:0;color:#0f172a;font-size:27px;line-height:1.2;font-weight:800;letter-spacing:0.1px;">${escapeHtml(safeHeading)}</h1>` : '';
172
+ const greetingBlock = safeGreeting ? `<p style="margin:0 0 10px;color:#0f172a;font-size:16px;font-weight:700;line-height:1.6;">${escapeHtml(safeGreeting)}</p>` : '';
173
+ const introBlock = safeIntro ? `<p style="margin:0 0 14px;color:#334155;font-size:15px;line-height:1.7;">${escapeHtml(safeIntro)}</p>` : '';
135
174
  const bodyBlock = renderParagraphsHtml(body);
136
175
 
137
176
  const ctaBlock =
138
177
  safeCtaUrl && safeCtaLabel
139
178
  ? `
140
- <table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:10px 0 10px;">
141
- <tr>
142
- <td align="center" bgcolor="#1d4ed8" style="border-radius:10px;">
143
- <a href="${escapeHtml(safeCtaUrl)}" style="display:inline-block;padding:12px 20px;font-size:15px;line-height:1.2;font-weight:700;color:#ffffff;text-decoration:none;">${escapeHtml(safeCtaLabel)}</a>
144
- </td>
145
- </tr>
146
- </table>
147
- `.trim()
179
+ <table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:14px 0 8px;">
180
+ <tr>
181
+ <td align="center" bgcolor="#1d4ed8" style="border-radius:10px;">
182
+ <a href="${escapeHtml(safeCtaUrl)}" style="display:inline-block;padding:13px 22px;font-size:15px;line-height:1.2;font-weight:700;color:#ffffff;text-decoration:none;letter-spacing:0.2px;">${escapeHtml(safeCtaLabel)}</a>
183
+ </td>
184
+ </tr>
185
+ </table>
186
+ `.trim()
148
187
  : '';
149
188
 
150
- const ctaHintBlock = safeCtaHint ? `<p style="margin:4px 0 0;color:#64748b;font-size:13px;line-height:1.55;">${escapeHtml(safeCtaHint)}</p>` : '';
151
- const secondaryCtaBlock = safeSecondaryCtaLabel && safeSecondaryCtaUrl ? `<p style="margin:10px 0 0;color:#1e293b;font-size:14px;line-height:1.6;">${escapeHtml(safeSecondaryCtaLabel)}: <a href="${escapeHtml(safeSecondaryCtaUrl)}" style="color:#2563eb;text-decoration:none;">${escapeHtml(safeSecondaryCtaUrl)}</a></p>` : '';
152
- const fallbackLinkBlock = safeCtaUrl ? `<p style="margin:12px 0 0;color:#64748b;font-size:12px;line-height:1.6;word-break:break-all;">Se o botão não funcionar, copie e cole este link no navegador: ${escapeHtml(safeCtaUrl)}</p>` : '';
153
- const securityNoteBlock = safeSecurityNote ? `<p style="margin:16px 0 0;padding:10px 12px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;color:#475569;font-size:12px;line-height:1.6;">${escapeHtml(safeSecurityNote)}</p>` : '';
154
-
189
+ const ctaHintBlock = safeCtaHint ? `<p style="margin:6px 0 0;color:#64748b;font-size:13px;line-height:1.6;">${escapeHtml(safeCtaHint)}</p>` : '';
190
+ const secondaryCtaBlock =
191
+ safeSecondaryCtaLabel && safeSecondaryCtaUrl
192
+ ? `
193
+ <p style="margin:12px 0 0;color:#334155;font-size:13px;line-height:1.65;">
194
+ ${escapeHtml(safeSecondaryCtaLabel)}:
195
+ <a href="${escapeHtml(safeSecondaryCtaUrl)}" style="color:#1d4ed8;text-decoration:none;font-weight:600;">${escapeHtml(safeSecondaryCtaUrl)}</a>
196
+ </p>
197
+ `.trim()
198
+ : '';
199
+ const fallbackLinkBlock = safeCtaUrl ? `<p style="margin:14px 0 0;color:#64748b;font-size:12px;line-height:1.7;word-break:break-all;">Se o botão não funcionar, copie e cole este link no navegador: ${escapeHtml(safeCtaUrl)}</p>` : '';
200
+ const securityNoteBlock = safeSecurityNote ? `<p style="margin:16px 0 0;padding:12px 13px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px;color:#475569;font-size:12px;line-height:1.65;">${escapeHtml(safeSecurityNote)}</p>` : '';
201
+ const brandTaglineBlock = brand.brandTagline ? `<p style="margin:8px 0 0;color:#cbd5e1;font-size:13px;line-height:1.6;">${escapeHtml(brand.brandTagline)}</p>` : '';
155
202
  const supportEmailLine = brand.supportEmail ? `<span style="display:block;margin-top:6px;">E-mail: <a href="mailto:${escapeHtml(brand.supportEmail)}" style="color:#2563eb;text-decoration:none;">${escapeHtml(brand.supportEmail)}</a></span>` : '';
156
- const footerMessageBlock = safeFooterMessage ? `<span style="display:block;margin-top:6px;color:#64748b;">${escapeHtml(safeFooterMessage)}</span>` : '';
157
- const taglineBlock = brand.brandTagline ? `<p style="margin:8px 0 0;color:#64748b;font-size:13px;line-height:1.6;">${escapeHtml(brand.brandTagline)}</p>` : '';
203
+ const footerMessageBlock = safeFooterMessage ? `<span style="display:block;margin-top:7px;color:#64748b;">${escapeHtml(safeFooterMessage)}</span>` : '';
158
204
 
159
205
  return `
160
206
  <!doctype html>
161
207
  <html lang="pt-BR">
162
- <body style="margin:0;padding:0;background:#f1f5f9;font-family:'Segoe UI',Roboto,Helvetica,Arial,sans-serif;">
208
+ <body style="margin:0;padding:0;background:#eef2f8;font-family:'Segoe UI',Roboto,Helvetica,Arial,sans-serif;">
163
209
  <div style="display:none;max-height:0;overflow:hidden;opacity:0;color:transparent;">${escapeHtml(safePreheader)}</div>
164
- <table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#f1f5f9;padding:28px 10px;">
210
+ <table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#eef2f8;padding:26px 10px;">
165
211
  <tr>
166
212
  <td align="center">
167
213
  <table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="max-width:640px;">
168
214
  <tr>
169
- <td align="center" style="padding:0 0 16px;">
170
- ${logoBlock}
171
- ${taglineBlock}
215
+ <td style="background:#0f172a;border-radius:16px 16px 0 0;padding:22px 24px 20px;">
216
+ <table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%">
217
+ <tr>
218
+ <td align="left" valign="middle" style="padding-right:12px;">
219
+ ${logoBlock}
220
+ ${brandTaglineBlock}
221
+ </td>
222
+ <td align="right" valign="middle" style="white-space:nowrap;">
223
+ <span style="display:inline-block;padding:6px 10px;border:1px solid rgba(148,163,184,0.45);border-radius:999px;font-size:11px;font-weight:700;letter-spacing:0.35px;text-transform:uppercase;color:#e2e8f0;">Comunicado oficial</span>
224
+ </td>
225
+ </tr>
226
+ </table>
227
+ </td>
228
+ </tr>
229
+ <tr>
230
+ <td style="background:#ffffff;border-left:1px solid #d9e2ef;border-right:1px solid #d9e2ef;padding:26px 24px 10px;">
231
+ ${headingBlock}
172
232
  </td>
173
233
  </tr>
174
234
  <tr>
175
- <td style="background:#ffffff;border:1px solid #dbe3ef;border-radius:14px;padding:26px 24px;box-shadow:0 4px 24px rgba(15,23,42,0.06);">
176
- <h1 style="margin:0 0 14px;color:#0f172a;font-size:25px;line-height:1.25;">${escapeHtml(safeHeading)}</h1>
235
+ <td style="background:#ffffff;border-left:1px solid #d9e2ef;border-right:1px solid #d9e2ef;padding:0 24px 22px;">
177
236
  ${greetingBlock}
178
237
  ${introBlock}
179
238
  ${bodyBlock}
@@ -185,11 +244,12 @@ const renderEmailLayout = ({ payload = {}, preheader = '', heading = '', greetin
185
244
  </td>
186
245
  </tr>
187
246
  <tr>
188
- <td style="padding:14px 2px 0;color:#64748b;font-size:12px;line-height:1.7;text-align:left;">
247
+ <td style="background:#ffffff;border:1px solid #d9e2ef;border-top:none;border-radius:0 0 16px 16px;padding:14px 24px 18px;color:#64748b;font-size:12px;line-height:1.7;">
189
248
  <span style="display:block;">${escapeHtml(brand.brandName)} © ${year}. Todos os direitos reservados.</span>
190
249
  <span style="display:block;margin-top:6px;">Central de suporte: <a href="${escapeHtml(brand.supportUrl)}" style="color:#2563eb;text-decoration:none;">${escapeHtml(brand.supportLabel)}</a></span>
191
250
  ${supportEmailLine}
192
251
  ${footerMessageBlock}
252
+ <span style="display:block;margin-top:7px;color:#94a3b8;font-size:11px;">Gerado em ${escapeHtml(generatedAt)} UTC.</span>
193
253
  </td>
194
254
  </tr>
195
255
  </table>
@@ -216,7 +276,7 @@ const resolveNavigationLinks = (payload = {}) => {
216
276
  };
217
277
 
218
278
  const resolveWelcomeBotWhatsApp = (payload = {}) => {
219
- const botPhoneCandidate = normalizePhoneDigits(payload?.botPhone || payload?.botNumber || process.env.EMAIL_WELCOME_BOT_PHONE || process.env.WHATSAPP_BOT_NUMBER || process.env.BOT_NUMBER || process.env.BOT_PHONE_NUMBER || process.env.PHONE_NUMBER || process.env.EMAIL_BRAND_SUPPORT_PHONE || '', 20);
279
+ const botPhoneCandidate = normalizePhoneDigits(payload?.botPhone || payload?.botNumber || process.env.EMAIL_WELCOME_BOT_PHONE || resolveBotPhoneFromEnv({ fallback: resolveSupportPhoneFromEnv({ fallback: '' }) }) || '', 20);
220
280
  const botPhoneDigits = isLikelyPhoneDigits(botPhoneCandidate) ? botPhoneCandidate : '';
221
281
  const botPhonePn = formatPhonePn(botPhoneDigits);
222
282
  const botWhatsAppUrl = botPhoneDigits ? `https://wa.me/${botPhoneDigits}` : '';
@@ -456,19 +516,66 @@ const TEMPLATE_BUILDERS = {
456
516
  };
457
517
 
458
518
  export const renderEmailTemplate = (templateKey, payload = {}) => {
519
+ const renderStartedAtMs = __timeNowMs();
459
520
  const normalizedTemplateKey = normalizeTemplateKey(templateKey);
460
- if (!normalizedTemplateKey) return null;
521
+ const payloadKeys = summarizePayloadKeys(payload);
522
+
523
+ if (!normalizedTemplateKey) {
524
+ logger.warn('Render de template de e-mail ignorado por chave inválida.', {
525
+ action: 'email_template_render_invalid_key',
526
+ template_key_raw: clipTemplatePreview(templateKey, 64),
527
+ payload_keys: payloadKeys,
528
+ });
529
+ return null;
530
+ }
461
531
  const builder = TEMPLATE_BUILDERS[normalizedTemplateKey];
462
- if (typeof builder !== 'function') return null;
532
+ if (typeof builder !== 'function') {
533
+ logger.warn('Template de e-mail não encontrado.', {
534
+ action: 'email_template_builder_not_found',
535
+ template_key: normalizedTemplateKey,
536
+ payload_keys: payloadKeys,
537
+ });
538
+ return null;
539
+ }
463
540
 
464
541
  const rendered = builder(payload || {});
465
- if (!rendered) return null;
542
+ if (!rendered) {
543
+ logger.warn('Template de e-mail retornou conteúdo vazio.', {
544
+ action: 'email_template_builder_empty',
545
+ template_key: normalizedTemplateKey,
546
+ payload_keys: payloadKeys,
547
+ });
548
+ return null;
549
+ }
466
550
 
467
551
  const subject = normalizeText(rendered.subject, 180);
468
552
  const text = normalizeText(rendered.text, 120_000);
469
553
  const html = normalizeText(rendered.html, 500_000);
470
554
 
471
- if (!subject || (!text && !html)) return null;
555
+ if (!subject || (!text && !html)) {
556
+ logger.warn('Template de e-mail inválido após normalização.', {
557
+ action: 'email_template_render_invalid_output',
558
+ template_key: normalizedTemplateKey,
559
+ subject_length: subject.length,
560
+ text_length: text.length,
561
+ html_length: html.length,
562
+ payload_keys: payloadKeys,
563
+ });
564
+ return null;
565
+ }
566
+
567
+ logger.debug('Template de e-mail renderizado com sucesso.', {
568
+ action: 'email_template_rendered',
569
+ template_key: normalizedTemplateKey,
570
+ subject_preview: clipTemplatePreview(subject),
571
+ subject_length: subject.length,
572
+ text_length: text.length,
573
+ html_length: html.length,
574
+ has_text: Boolean(text),
575
+ has_html: Boolean(html),
576
+ payload_keys: payloadKeys,
577
+ render_duration_ms: Math.max(0, __timeNowMs() - renderStartedAtMs),
578
+ });
472
579
 
473
580
  return {
474
581
  subject,
@@ -225,7 +225,7 @@ export const buildCookieString = (name, value, req, options = {}) => {
225
225
  return parts.join('; ');
226
226
  };
227
227
 
228
- export const readJsonBody = async (req, { maxBytes = 64 * 1024 } = {}) =>
228
+ export const readRawBody = async (req, { maxBytes = 64 * 1024 } = {}) =>
229
229
  new Promise((resolve, reject) => {
230
230
  const chunks = [];
231
231
  let total = 0;
@@ -243,20 +243,24 @@ export const readJsonBody = async (req, { maxBytes = 64 * 1024 } = {}) =>
243
243
  });
244
244
 
245
245
  req.on('end', () => {
246
- const raw = Buffer.concat(chunks).toString('utf8').trim();
247
- if (!raw) {
248
- resolve({});
249
- return;
250
- }
251
-
252
- try {
253
- resolve(JSON.parse(raw));
254
- } catch {
255
- const error = new Error('JSON invalido.');
256
- error.statusCode = 400;
257
- reject(error);
258
- }
246
+ resolve(Buffer.concat(chunks));
259
247
  });
260
248
 
261
249
  req.on('error', (error) => reject(error));
262
250
  });
251
+
252
+ export const readJsonBody = async (req, { maxBytes = 64 * 1024 } = {}) => {
253
+ const rawBuffer = await readRawBody(req, { maxBytes });
254
+ const raw = rawBuffer.toString('utf8').trim();
255
+ if (!raw) {
256
+ return {};
257
+ }
258
+
259
+ try {
260
+ return JSON.parse(raw);
261
+ } catch {
262
+ const error = new Error('JSON invalido.');
263
+ error.statusCode = 400;
264
+ throw error;
265
+ }
266
+ };
@@ -6,6 +6,7 @@ import { getMetricsServerConfig, isMetricsEnabled, recordHttpRequest, resolveRou
6
6
  import { applyCachePolicy } from '../middleware/cachePolicy.js';
7
7
  import { applySensitiveRouteRateLimit } from '../middleware/endpointRateLimit.js';
8
8
  import { applySecurityHeaders } from '../middleware/securityHeaders.js';
9
+ import { shouldHandleGrafanaProxyPath } from '../routes/observability/grafanaProxyRouter.js';
9
10
  import { getIndexRouteConfigs, routeRequest } from '../routes/indexRouter.js';
10
11
  import { parseRequestUrl, normalizeRequestId } from './requestContext.js';
11
12
 
@@ -65,6 +66,7 @@ export const startHttpServer = () => {
65
66
  userConfig: routeConfigs?.userConfig || null,
66
67
  systemAdminConfig: routeConfigs?.systemAdminConfig || null,
67
68
  });
69
+ const isGrafanaProxyRequest = shouldHandleGrafanaProxyPath(pathname, routeConfigs?.grafanaProxyConfig || null);
68
70
 
69
71
  res.once('finish', () => {
70
72
  recordHttpRequest({
@@ -76,10 +78,12 @@ export const startHttpServer = () => {
76
78
  });
77
79
 
78
80
  try {
79
- applySecurityHeaders(req, res);
80
- applyCachePolicy(req, res, { pathname });
81
- const allowedByRateLimit = await applySensitiveRouteRateLimit(req, res, { pathname });
82
- if (!allowedByRateLimit) return;
81
+ if (!isGrafanaProxyRequest) {
82
+ applySecurityHeaders(req, res);
83
+ applyCachePolicy(req, res, { pathname });
84
+ const allowedByRateLimit = await applySensitiveRouteRateLimit(req, res, { pathname });
85
+ if (!allowedByRateLimit) return;
86
+ }
83
87
 
84
88
  await routeRequest(req, res, {
85
89
  pathname,
@@ -10,23 +10,44 @@ const parseEnvBool = (value, fallback = false) => {
10
10
  return fallback;
11
11
  };
12
12
 
13
+ const parseEnvList = (value) =>
14
+ String(value || '')
15
+ .split(',')
16
+ .map((item) => String(item || '').trim())
17
+ .filter(Boolean);
18
+
19
+ const toHttpOrigin = (value) => {
20
+ const raw = String(value || '').trim();
21
+ if (!raw) return '';
22
+ try {
23
+ const parsed = new URL(raw);
24
+ if (!['http:', 'https:'].includes(parsed.protocol)) return '';
25
+ return parsed.origin;
26
+ } catch {
27
+ return '';
28
+ }
29
+ };
30
+
13
31
  const HELMET_CSP_ENFORCE = parseEnvBool(process.env.HELMET_CONTENT_SECURITY_POLICY_ENABLED, true);
14
32
  const BACKEND_BUILD_ID = String(process.env.OMNIZAP_BUILD_ID || '')
15
33
  .trim()
16
34
  .slice(0, 80);
35
+ const FRAME_SRC_EXTRA = Array.from(new Set([...parseEnvList(process.env.HELMET_CSP_FRAME_SRC_EXTRA), process.env.SYSTEM_ADMIN_GRAFANA_URL, process.env.GRAFANA_PUBLIC_URL].map((item) => toHttpOrigin(item)).filter(Boolean)));
17
36
 
18
37
  const HELMET_CSP_DIRECTIVES = {
19
38
  defaultSrc: ["'self'"],
20
39
  baseUri: ["'self'"],
21
40
  objectSrc: ["'none'"],
22
41
  frameAncestors: ["'self'"],
23
- formAction: ["'self'"],
42
+ // Google Identity Services usa submit interno para accounts.google.com/gsi/transform.
43
+ // Sem essa origem no form-action o login pode travar na etapa de transform.
44
+ formAction: ["'self'", 'https://accounts.google.com'],
24
45
  scriptSrc: ["'self'", "'unsafe-inline'", 'https://accounts.google.com', 'https://cdn.tailwindcss.com'],
25
46
  styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com', 'https://cdnjs.cloudflare.com'],
26
47
  imgSrc: ["'self'", 'data:', 'blob:', 'https:'],
27
48
  fontSrc: ["'self'", 'data:', 'https://fonts.gstatic.com', 'https://cdnjs.cloudflare.com'],
28
49
  connectSrc: ["'self'", 'https://accounts.google.com', 'https://oauth2.googleapis.com', 'https://api.github.com'],
29
- frameSrc: ["'self'", 'https://accounts.google.com'],
50
+ frameSrc: ["'self'", 'https://accounts.google.com', ...FRAME_SRC_EXTRA],
30
51
  workerSrc: ["'self'", 'blob:'],
31
52
  manifestSrc: ["'self'"],
32
53
  };
@@ -52,6 +73,11 @@ const helmetMiddleware = helmet({
52
73
  directives: HELMET_CSP_DIRECTIVES,
53
74
  reportOnly: !HELMET_CSP_ENFORCE,
54
75
  },
76
+ // Fluxos OAuth/FedCM em popup (Google GIS) podem quebrar com COOP strict.
77
+ // Permitimos opener em popups mantendo isolamento para navegação principal.
78
+ crossOriginOpenerPolicy: {
79
+ policy: 'same-origin-allow-popups',
80
+ },
55
81
  crossOriginEmbedderPolicy: false,
56
82
  // Mantemos permissões explícitas para browser APIs sensíveis.
57
83
  permissionsPolicy: {
@@ -59,6 +85,7 @@ const helmetMiddleware = helmet({
59
85
  geolocation: [],
60
86
  microphone: [],
61
87
  camera: [],
88
+ 'identity-credentials-get': ['self'],
62
89
  },
63
90
  },
64
91
  });
@@ -67,7 +94,12 @@ const applyFallbackHeaders = (res) => {
67
94
  if (!res.getHeader('X-Content-Type-Options')) res.setHeader('X-Content-Type-Options', 'nosniff');
68
95
  if (!res.getHeader('X-Frame-Options')) res.setHeader('X-Frame-Options', 'DENY');
69
96
  if (!res.getHeader('Referrer-Policy')) res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
70
- if (!res.getHeader('Permissions-Policy')) res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
97
+ if (!res.getHeader('Permissions-Policy')) {
98
+ res.setHeader(
99
+ 'Permissions-Policy',
100
+ 'geolocation=(), microphone=(), camera=(), identity-credentials-get=(self)',
101
+ );
102
+ }
71
103
  if (FALLBACK_CSP_HEADER && !res.getHeader('Content-Security-Policy') && !res.getHeader('Content-Security-Policy-Report-Only')) {
72
104
  const cspHeaderName = HELMET_CSP_ENFORCE ? 'Content-Security-Policy' : 'Content-Security-Policy-Report-Only';
73
105
  res.setHeader(cspHeaderName, FALLBACK_CSP_HEADER);
@@ -24,8 +24,10 @@ const DEFAULT_USER_SYSTEM_ADMIN_WEB_PATH = '/user/systemadm';
24
24
  const DEFAULT_LEGACY_STICKER_ADMIN_WEB_PATH = '/stickers/admin';
25
25
  const DEFAULT_SYSTEM_ADMIN_API_BASE_PATH = '/api/admin';
26
26
  const DEFAULT_SYSTEM_ADMIN_API_SESSION_PATH = '/api/admin/session';
27
+ const DEFAULT_SYSTEM_ADMIN_API_MULTI_SESSION_PATH = '/api/admin/multi-session';
27
28
  const DEFAULT_LEGACY_STICKER_ADMIN_API_BASE_PATH = '/api/sticker-packs/admin';
28
29
  const DEFAULT_LEGACY_STICKER_ADMIN_API_SESSION_PATH = '/api/sticker-packs/admin/session';
30
+ const DEFAULT_LEGACY_STICKER_ADMIN_API_MULTI_SESSION_PATH = '/api/sticker-packs/admin/multi-session';
29
31
 
30
32
  export const getSystemAdminRouterConfig = async () => {
31
33
  const controller = await loadSystemAdminController();
@@ -35,8 +37,10 @@ export const getSystemAdminRouterConfig = async () => {
35
37
  legacyWebPath: normalizeBasePath(legacyConfig.legacyWebPath, DEFAULT_LEGACY_STICKER_ADMIN_WEB_PATH),
36
38
  apiAdminBasePath: normalizeBasePath(legacyConfig.apiAdminBasePath, DEFAULT_SYSTEM_ADMIN_API_BASE_PATH),
37
39
  apiAdminSessionPath: normalizeBasePath(legacyConfig.apiAdminSessionPath, DEFAULT_SYSTEM_ADMIN_API_SESSION_PATH),
40
+ apiAdminMultiSessionPath: normalizeBasePath(legacyConfig.apiAdminMultiSessionPath, DEFAULT_SYSTEM_ADMIN_API_MULTI_SESSION_PATH),
38
41
  legacyApiAdminBasePath: normalizeBasePath(legacyConfig.legacyApiAdminBasePath, DEFAULT_LEGACY_STICKER_ADMIN_API_BASE_PATH),
39
42
  legacyApiAdminSessionPath: normalizeBasePath(legacyConfig.legacyApiAdminSessionPath, DEFAULT_LEGACY_STICKER_ADMIN_API_SESSION_PATH),
43
+ legacyApiAdminMultiSessionPath: normalizeBasePath(legacyConfig.legacyApiAdminMultiSessionPath, DEFAULT_LEGACY_STICKER_ADMIN_API_MULTI_SESSION_PATH),
40
44
  };
41
45
  };
42
46
 
@@ -46,8 +50,10 @@ export const shouldHandleSystemAdminPath = (pathname, systemAdminConfig = null)
46
50
  legacyWebPath: DEFAULT_LEGACY_STICKER_ADMIN_WEB_PATH,
47
51
  apiAdminBasePath: DEFAULT_SYSTEM_ADMIN_API_BASE_PATH,
48
52
  apiAdminSessionPath: DEFAULT_SYSTEM_ADMIN_API_SESSION_PATH,
53
+ apiAdminMultiSessionPath: DEFAULT_SYSTEM_ADMIN_API_MULTI_SESSION_PATH,
49
54
  legacyApiAdminBasePath: DEFAULT_LEGACY_STICKER_ADMIN_API_BASE_PATH,
50
55
  legacyApiAdminSessionPath: DEFAULT_LEGACY_STICKER_ADMIN_API_SESSION_PATH,
56
+ legacyApiAdminMultiSessionPath: DEFAULT_LEGACY_STICKER_ADMIN_API_MULTI_SESSION_PATH,
51
57
  };
52
58
 
53
59
  if (startsWithPath(pathname, resolvedConfig.webPath)) return true;
@@ -3,12 +3,14 @@ import path from 'node:path';
3
3
  import { maybeHandleMetricsRequest } from './metrics/metricsRouter.js';
4
4
  import { maybeHandleHealthRequest, shouldHandleHealthPath } from './health/healthRouter.js';
5
5
  import { getEmailAutomationRouterConfig, maybeHandleEmailAutomationRequest, shouldHandleEmailAutomationPath } from './email/emailAutomationRouter.js';
6
+ import { getPaymentsRouterConfig, maybeHandlePaymentsRequest, shouldHandlePaymentsPath } from './payments/paymentsRouter.js';
6
7
  import { buildUserApiPaths, getUserRouterConfig, maybeHandleUserRequest, shouldHandleUserPath } from './user/userRouter.js';
7
8
  import { getSystemAdminRouterConfig, maybeHandleSystemAdminRequest, shouldHandleSystemAdminPath } from './admin/systemAdminRouter.js';
8
9
  import { getStickerSiteRouterConfig, maybeHandleStickerSiteRequest, shouldHandleStickerSitePath } from './sticker/stickerSiteRouter.js';
9
10
  import { getStickerDataRouterConfig, maybeHandleStickerDataRequest, shouldHandleStickerDataPath } from './sticker/stickerDataRouter.js';
10
11
  import { getStickerApiRouterConfig, maybeHandleStickerApiRequest, shouldHandleStickerApiPath } from './sticker/stickerApiRouter.js';
11
12
  import { maybeHandleStaticPageRequest, shouldHandleStaticPagePath } from './static/staticPageRouter.js';
13
+ import { getGrafanaProxyRouterConfig, maybeHandleGrafanaProxyRequest, shouldHandleGrafanaProxyPath } from './observability/grafanaProxyRouter.js';
12
14
 
13
15
  const startsWithPath = (pathname, prefix) => {
14
16
  if (!pathname || !prefix) return false;
@@ -75,6 +77,16 @@ const loadEmailAutomationConfigSafe = async () => {
75
77
  }
76
78
  };
77
79
 
80
+ const loadPaymentsConfigSafe = async () => {
81
+ try {
82
+ return await getPaymentsRouterConfig();
83
+ } catch {
84
+ return {
85
+ apiBasePath: '/api/payments',
86
+ };
87
+ }
88
+ };
89
+
78
90
  const loadStickerSiteConfigSafe = async () => {
79
91
  try {
80
92
  return await getStickerSiteRouterConfig();
@@ -112,12 +124,28 @@ const loadStickerApiConfigSafe = async () => {
112
124
  }
113
125
  };
114
126
 
127
+ const loadGrafanaProxyConfigSafe = async () => {
128
+ try {
129
+ return getGrafanaProxyRouterConfig();
130
+ } catch {
131
+ return {
132
+ enabled: false,
133
+ basePath: '/api/grafana',
134
+ legacyBasePath: '/grafana',
135
+ timeoutMs: 15000,
136
+ target: null,
137
+ };
138
+ }
139
+ };
140
+
115
141
  export const getIndexRouteConfigs = async () => {
116
142
  if (!indexRouteConfigsPromise) {
117
- indexRouteConfigsPromise = Promise.all([loadUserConfigSafe(), loadSystemAdminConfigSafe(), loadEmailAutomationConfigSafe(), loadStickerSiteConfigSafe(), loadStickerDataConfigSafe(), loadStickerApiConfigSafe()]).then(([userConfig, systemAdminConfig, emailAutomationConfig, stickerSiteConfig, stickerDataConfig, stickerApiConfig]) => ({
143
+ indexRouteConfigsPromise = Promise.all([loadUserConfigSafe(), loadSystemAdminConfigSafe(), loadEmailAutomationConfigSafe(), loadPaymentsConfigSafe(), loadStickerSiteConfigSafe(), loadStickerDataConfigSafe(), loadStickerApiConfigSafe(), loadGrafanaProxyConfigSafe()]).then(([userConfig, systemAdminConfig, emailAutomationConfig, paymentsConfig, stickerSiteConfig, stickerDataConfig, stickerApiConfig, grafanaProxyConfig]) => ({
118
144
  userConfig,
119
145
  systemAdminConfig,
120
146
  emailAutomationConfig,
147
+ paymentsConfig,
148
+ grafanaProxyConfig,
121
149
  stickerConfig: {
122
150
  ...stickerSiteConfig,
123
151
  ...stickerDataConfig,
@@ -140,6 +168,8 @@ export const routeRequest = async (req, res, { pathname, url, metricsPath = '/me
140
168
  const userConfig = resolvedConfigs?.userConfig || null;
141
169
  const systemAdminConfig = resolvedConfigs?.systemAdminConfig || null;
142
170
  const emailAutomationConfig = resolvedConfigs?.emailAutomationConfig || null;
171
+ const paymentsConfig = resolvedConfigs?.paymentsConfig || null;
172
+ const grafanaProxyConfig = resolvedConfigs?.grafanaProxyConfig || null;
143
173
  const stickerConfig = resolvedConfigs?.stickerConfig || null;
144
174
 
145
175
  // 1) Metrics
@@ -163,7 +193,21 @@ export const routeRequest = async (req, res, { pathname, url, metricsPath = '/me
163
193
  return sendNotFound(req, res);
164
194
  }
165
195
 
166
- // 4) User
196
+ // 4) Payments API
197
+ if (shouldHandlePaymentsPath(pathname, paymentsConfig)) {
198
+ const handled = await maybeHandlePaymentsRequest(req, res, { pathname, url });
199
+ if (handled) return true;
200
+ return sendNotFound(req, res);
201
+ }
202
+
203
+ // 5) Grafana proxy (/api/grafana e alias /grafana)
204
+ if (shouldHandleGrafanaProxyPath(pathname, grafanaProxyConfig)) {
205
+ const handled = await maybeHandleGrafanaProxyRequest(req, res, { pathname, url, config: grafanaProxyConfig });
206
+ if (handled) return true;
207
+ return sendNotFound(req, res);
208
+ }
209
+
210
+ // 6) User
167
211
  const systemAdminCandidate = shouldHandleSystemAdminStep(pathname, systemAdminConfig);
168
212
  if (shouldHandleUserStep(pathname, userConfig)) {
169
213
  const handled = await maybeHandleUserRequest(req, res, { pathname, url });
@@ -173,14 +217,14 @@ export const routeRequest = async (req, res, { pathname, url, metricsPath = '/me
173
217
  if (!systemAdminCandidate) return sendNotFound(req, res);
174
218
  }
175
219
 
176
- // 5) System admin + legacy /stickers/admin
220
+ // 7) System admin + legacy /stickers/admin
177
221
  if (systemAdminCandidate) {
178
222
  const handled = await maybeHandleSystemAdminRequest(req, res, { pathname, url });
179
223
  if (handled) return true;
180
224
  return sendNotFound(req, res);
181
225
  }
182
226
 
183
- // 6) Sticker catalog apenas nos prefixes permitidos
227
+ // 8) Sticker catalog apenas nos prefixes permitidos
184
228
  if (shouldHandleStickerSitePath(pathname, stickerConfig)) {
185
229
  const handled = await maybeHandleStickerSiteRequest(req, res, { pathname, url });
186
230
  if (handled) return true;
@@ -212,14 +256,14 @@ export const routeRequest = async (req, res, { pathname, url, metricsPath = '/me
212
256
  return sendNotFound(req, res);
213
257
  }
214
258
 
215
- // 7) Paginas estaticas (templates em public/pages)
259
+ // 9) Paginas estaticas (templates em public/pages)
216
260
  if (shouldHandleStaticPagePath(pathname)) {
217
261
  const handled = await maybeHandleStaticPageRequest(req, res, { pathname });
218
262
  if (handled) return true;
219
263
  return sendNotFound(req, res);
220
264
  }
221
265
 
222
- // 8) 404 global
266
+ // 10) 404 global
223
267
  return sendNotFound(req, res);
224
268
  };
225
269