@omnizap-system/omnizap 2.6.0 → 2.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/.env.example +58 -13
  2. package/.github/workflows/ci.yml +5 -5
  3. package/.github/workflows/codeql.yml +1 -1
  4. package/.github/workflows/db-migration-check.yml +2 -2
  5. package/.github/workflows/dependency-review.yml +1 -1
  6. package/.github/workflows/deploy.yml +2 -2
  7. package/.github/workflows/release.yml +2 -2
  8. package/.github/workflows/security-attest-provenance.yml +2 -2
  9. package/.github/workflows/security-gitleaks.yml +13 -4
  10. package/.github/workflows/security-runner-hardening.yml +2 -2
  11. package/.github/workflows/security-scorecard.yml +1 -1
  12. package/.github/workflows/security-zap-baseline.yml +1 -1
  13. package/.github/workflows/security-zap-full-scan.yml +2 -1
  14. package/.github/workflows/security-zizmor.yml +1 -1
  15. package/.github/workflows/wiki-sync.yml +1 -1
  16. package/.gitleaksignore +9 -0
  17. package/CODE_OF_CONDUCT.md +2 -2
  18. package/GEMINI.md +64 -0
  19. package/README.md +52 -82
  20. package/SECURITY.md +1 -1
  21. package/app/config/index.js +2 -0
  22. package/app/configParts/adminIdentity.js +5 -5
  23. package/app/configParts/baileysConfig.js +230 -58
  24. package/app/configParts/groupUtils.js +5 -0
  25. package/app/configParts/messagePersistenceService.js +145 -4
  26. package/app/configParts/sessionConfig.js +157 -0
  27. package/app/connection/baileysCompatibility.test.js +1 -1
  28. package/app/connection/groupOwnerWriteStateResolver.js +109 -0
  29. package/app/connection/socketController.js +660 -158
  30. package/app/connection/socketController.multiSession.test.js +108 -0
  31. package/app/controllers/messageController.js +1 -1
  32. package/app/controllers/messagePipeline/commandMiddleware.js +12 -10
  33. package/app/controllers/messagePipeline/conversationMiddleware.js +2 -1
  34. package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +104 -0
  35. package/app/controllers/messagePipeline/preProcessingMiddlewares.js +80 -2
  36. package/app/controllers/messageProcessingPipeline.js +93 -13
  37. package/app/controllers/messageProcessingPipeline.test.js +200 -0
  38. package/app/modules/adminModule/AGENT.md +1 -1
  39. package/app/modules/adminModule/commandConfig.json +3318 -1347
  40. package/app/modules/adminModule/groupCommandHandlers.js +858 -15
  41. package/app/modules/adminModule/groupCommandHandlers.test.js +378 -11
  42. package/app/modules/adminModule/groupWarningRepository.js +152 -0
  43. package/app/modules/aiModule/AGENT.md +47 -30
  44. package/app/modules/aiModule/aiConfigRuntime.js +1 -0
  45. package/app/modules/aiModule/catCommand.js +135 -27
  46. package/app/modules/aiModule/commandConfig.json +114 -28
  47. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +54 -6
  48. package/app/modules/gameModule/AGENT.md +1 -1
  49. package/app/modules/gameModule/commandConfig.json +29 -0
  50. package/app/modules/menuModule/AGENT.md +1 -1
  51. package/app/modules/menuModule/commandConfig.json +45 -10
  52. package/app/modules/menuModule/menuCatalogService.js +190 -0
  53. package/app/modules/menuModule/menuCommandUsageRepository.js +109 -0
  54. package/app/modules/menuModule/menuDynamicService.js +511 -0
  55. package/app/modules/menuModule/menuDynamicService.test.js +141 -0
  56. package/app/modules/menuModule/menus.js +36 -5
  57. package/app/modules/playModule/AGENT.md +10 -5
  58. package/app/modules/playModule/commandConfig.json +140 -12
  59. package/app/modules/playModule/playCommand.js +1 -1417
  60. package/app/modules/playModule/playCommandConstants.js +80 -0
  61. package/app/modules/playModule/playCommandCore.js +361 -0
  62. package/app/modules/playModule/playCommandHandlers.js +41 -0
  63. package/app/modules/playModule/playCommandMediaClient.js +1872 -0
  64. package/app/modules/playModule/playConfigRuntime.js +245 -4
  65. package/app/modules/playModule/playModuleCriticalFlows.test.js +152 -0
  66. package/app/modules/quoteModule/AGENT.md +1 -1
  67. package/app/modules/quoteModule/commandConfig.json +29 -0
  68. package/app/modules/quoteModule/quoteCommand.js +3 -2
  69. package/app/modules/rpgPokemonModule/AGENT.md +1 -1
  70. package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
  71. package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +5 -4
  72. package/app/modules/rpgPokemonModule/rpgBattleService.test.js +2 -1
  73. package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +2 -1
  74. package/app/modules/rpgPokemonModule/rpgPokemonService.js +38 -37
  75. package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +4 -3
  76. package/app/modules/statsModule/AGENT.md +1 -1
  77. package/app/modules/statsModule/commandConfig.json +58 -0
  78. package/app/modules/statsModule/rankingCommon.js +5 -4
  79. package/app/modules/stickerModule/AGENT.md +1 -1
  80. package/app/modules/stickerModule/addStickerMetadata.js +4 -3
  81. package/app/modules/stickerModule/commandConfig.json +145 -0
  82. package/app/modules/stickerModule/stickerCommand.js +1 -1
  83. package/app/modules/stickerPackModule/AGENT.md +1 -1
  84. package/app/modules/stickerPackModule/autoPackCollectorService.js +5 -1
  85. package/app/modules/stickerPackModule/commandConfig.json +29 -0
  86. package/app/modules/stickerPackModule/semanticThemeClusterService.js +7 -6
  87. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +10 -9
  88. package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +9 -8
  89. package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +3 -2
  90. package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +2 -1
  91. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +80 -58
  92. package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +2 -1
  93. package/app/modules/stickerPackModule/stickerPackRepository.js +2 -1
  94. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +5 -4
  95. package/app/modules/stickerPackModule/stickerPackService.js +13 -6
  96. package/app/modules/stickerPackModule/stickerStorageService.js +3 -2
  97. package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +2 -1
  98. package/app/modules/systemMetricsModule/AGENT.md +1 -1
  99. package/app/modules/systemMetricsModule/commandConfig.json +29 -0
  100. package/app/modules/systemMetricsModule/pingCommand.js +6 -5
  101. package/app/modules/tiktokModule/AGENT.md +1 -1
  102. package/app/modules/tiktokModule/commandConfig.json +29 -0
  103. package/app/modules/tiktokModule/tiktokCommand.js +2 -1
  104. package/app/modules/userModule/AGENT.md +1 -1
  105. package/app/modules/userModule/commandConfig.json +29 -0
  106. package/app/modules/userModule/userCommand.js +72 -23
  107. package/app/modules/waifuPicsModule/AGENT.md +57 -27
  108. package/app/modules/waifuPicsModule/commandConfig.json +87 -0
  109. package/app/modules/waifuPicsModule/waifuPicsCommand.js +3 -2
  110. package/app/observability/metrics.js +136 -0
  111. package/app/services/ai/commandConfigEnrichmentService.js +229 -47
  112. package/app/services/ai/conversationRouterService.js +4 -3
  113. package/app/services/ai/geminiService.js +132 -7
  114. package/app/services/ai/geminiService.test.js +59 -2
  115. package/app/services/ai/globalModuleAiHelpService.js +3 -2
  116. package/app/services/ai/messageCommandExecutionService.js +2 -1
  117. package/app/services/ai/moduleAiHelpCoreService.js +45 -14
  118. package/app/services/ai/moduleToolExecutorService.js +3 -2
  119. package/app/services/ai/moduleToolRegistryService.js +2 -1
  120. package/app/services/ai/toolCandidateSelectorService.js +6 -5
  121. package/app/services/auth/googleWebLinkService.js +3 -2
  122. package/app/services/auth/whatsappLoginLinkService.js +3 -2
  123. package/app/services/external/pokeApiService.js +4 -3
  124. package/app/services/group/groupMetadataService.js +24 -1
  125. package/app/services/infra/dbWriteQueue.js +57 -26
  126. package/app/services/infra/featureFlagService.js +2 -1
  127. package/app/services/messaging/captchaService.js +3 -2
  128. package/app/services/messaging/newsBroadcastService.js +846 -29
  129. package/app/services/multiSession/assignmentBalancerService.js +457 -0
  130. package/app/services/multiSession/groupOwnershipRepository.js +381 -0
  131. package/app/services/multiSession/groupOwnershipService.js +890 -0
  132. package/app/services/multiSession/groupOwnershipService.test.js +309 -0
  133. package/app/services/multiSession/sessionRegistryService.js +293 -0
  134. package/app/services/sticker/stickerFocusService.js +11 -10
  135. package/app/store/aiPromptStore.js +36 -19
  136. package/app/store/conversationSessionStore.js +7 -6
  137. package/app/store/groupConfigStore.js +41 -5
  138. package/app/store/premiumUserStore.js +21 -7
  139. package/app/utils/antiLink/antiLinkModule.js +352 -16
  140. package/app/workers/aiHelperContinuousLearningWorker.js +512 -0
  141. package/app/workers/aiLearningWorker.js +6 -5
  142. package/app/workers/commandConfigEnrichmentWorker.js +4 -3
  143. package/database/index.js +14 -8
  144. package/database/migrations/20260307_d0_hardening_down.sql +1 -1
  145. package/database/migrations/20260314_d7_canonical_sender_down.sql +1 -1
  146. package/database/migrations/20260406_d30_security_analytics_down.sql +1 -1
  147. package/database/migrations/20260411_d35_group_community_metadata_down.sql +59 -0
  148. package/database/migrations/20260411_d35_group_community_metadata_up.sql +62 -0
  149. package/database/migrations/20260412_d36_system_config_tables_down.sql +32 -0
  150. package/database/migrations/20260412_d36_system_config_tables_up.sql +66 -0
  151. package/database/migrations/20260413_d37_group_user_warnings_down.sql +11 -0
  152. package/database/migrations/20260413_d37_group_user_warnings_up.sql +24 -0
  153. package/database/migrations/20260414_d38_multi_session_foundation_down.sql +72 -0
  154. package/database/migrations/20260414_d38_multi_session_foundation_up.sql +125 -0
  155. package/database/migrations/20260414_d39_multi_session_cutover_down.sql +103 -0
  156. package/database/migrations/20260414_d39_multi_session_cutover_up.sql +83 -0
  157. package/database/schema.sql +102 -1
  158. package/docker-compose.yml +4 -1
  159. package/docs/compliance/acceptable-use-policy-2026-03-07.md +1 -1
  160. package/docs/compliance/dpa-b2b-standard-2026-03-07.md +1 -1
  161. package/docs/compliance/privacy-policy-2026-03-07.md +4 -4
  162. package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
  163. package/docs/security/incident-response-lgpd-anpd-runbook-2026-03-07.md +1 -1
  164. package/docs/security/network-hardening-runbook-2026-03-07.md +53 -0
  165. package/docs/security/omnizap-static-security-headers.conf +25 -0
  166. package/docs/wiki/Home.md +1 -1
  167. package/ecosystem.prod.config.cjs +32 -12
  168. package/index.js +57 -23
  169. package/observability/alert-rules.yml +20 -0
  170. package/observability/grafana/dashboards/omnizap-system-admin.json +229 -0
  171. package/observability/mysql-setup.sql +4 -4
  172. package/observability/system-admin-observability.md +26 -0
  173. package/package.json +20 -6
  174. package/public/apple-touch-icon.png +0 -0
  175. package/public/comandos/commands-catalog.json +2853 -3326
  176. package/public/favicon-16x16.png +0 -0
  177. package/public/favicon-32x32.png +0 -0
  178. package/public/favicon.ico +0 -0
  179. package/public/js/apps/apiDocsApp.js +3 -2
  180. package/public/js/apps/commandsReactApp.js +280 -99
  181. package/public/js/apps/createPackApp.js +11 -10
  182. package/public/js/apps/homeReactApp.js +181 -130
  183. package/public/js/apps/loginReactApp.js +1 -1
  184. package/public/js/apps/stickersApp.js +263 -110
  185. package/public/js/apps/termsReactApp.js +73 -24
  186. package/public/js/apps/userApp.js +4 -3
  187. package/public/js/apps/userPasswordResetReactApp.js +406 -0
  188. package/public/js/apps/userReactApp.js +355 -280
  189. package/public/js/apps/userSystemAdmReactApp.js +1506 -0
  190. package/public/pages/api-docs.html +1 -1
  191. package/public/pages/aup.html +2 -2
  192. package/public/pages/dpa.html +3 -3
  193. package/public/pages/licenca.html +4 -4
  194. package/public/pages/login.html +1 -1
  195. package/public/pages/notice-and-takedown.html +2 -2
  196. package/public/pages/politica-de-privacidade.html +6 -6
  197. package/public/pages/seo-bot-whatsapp-para-grupo.html +3 -3
  198. package/public/pages/seo-bot-whatsapp-sem-programar.html +3 -3
  199. package/public/pages/seo-como-automatizar-avisos-no-whatsapp.html +3 -3
  200. package/public/pages/seo-como-criar-comandos-whatsapp.html +3 -3
  201. package/public/pages/seo-como-evitar-spam-no-whatsapp.html +3 -3
  202. package/public/pages/seo-como-moderar-grupo-whatsapp.html +3 -3
  203. package/public/pages/seo-como-organizar-comunidade-whatsapp.html +3 -3
  204. package/public/pages/seo-melhor-bot-whatsapp-para-grupos.html +3 -3
  205. package/public/pages/stickers-admin.html +1 -1
  206. package/public/pages/stickers-create.html +1 -1
  207. package/public/pages/stickers.html +6 -6
  208. package/public/pages/suboperadores.html +2 -2
  209. package/public/pages/termos-de-uso-texto-integral.html +6 -6
  210. package/public/pages/termos-de-uso.html +3 -3
  211. package/public/pages/user-password-reset.html +4 -5
  212. package/public/pages/user-systemadm.html +9 -463
  213. package/public/pages/user.html +2 -2
  214. package/scripts/clear-whatsapp-session.sh +123 -0
  215. package/scripts/core-ai-mode.mjs +163 -0
  216. package/scripts/deploy.sh +11 -1
  217. package/scripts/email-broadcast-terms-update.mjs +2 -1
  218. package/scripts/enrich-command-config-ux-openai.mjs +492 -0
  219. package/scripts/generate-commands-catalog.mjs +166 -2
  220. package/scripts/generate-module-agents.mjs +2 -1
  221. package/scripts/generate-seo-satellite-pages.mjs +5 -4
  222. package/scripts/github-deploy-notify.mjs +2 -1
  223. package/scripts/github-release-notify.mjs +25 -10
  224. package/scripts/new-whatsapp-session.sh +317 -0
  225. package/scripts/release.sh +2 -19
  226. package/scripts/security-smoketest.mjs +6 -5
  227. package/scripts/security-web-surface-check.mjs +218 -0
  228. package/scripts/sticker-catalog-loadtest.mjs +5 -4
  229. package/server/auth/googleWebAuth/googleWebAuthService.js +8 -7
  230. package/server/auth/jwt/webJwtService.js +1 -1
  231. package/server/auth/stickerCatalogAuthContext.js +2 -1
  232. package/server/auth/termsAcceptance/termsAcceptanceHandler.js +2 -1
  233. package/server/auth/userPassword/userPasswordAuthService.js +2 -1
  234. package/server/auth/userPassword/userPasswordRecoveryService.js +4 -3
  235. package/server/auth/webAccount/webAccountHandlers.js +9 -10
  236. package/server/controllers/admin/adminPanelHandlers.js +267 -16
  237. package/server/controllers/admin/systemAdminController.js +267 -0
  238. package/server/controllers/seo/stickerCatalogSeoContext.js +10 -9
  239. package/server/controllers/sticker/nonCatalogHandlers.js +2 -1
  240. package/server/controllers/sticker/stickerCatalogController.js +23 -36
  241. package/server/controllers/system/contactController.js +9 -17
  242. package/server/controllers/system/githubController.js +3 -2
  243. package/server/controllers/system/stickerCatalogSystemContext.js +41 -19
  244. package/server/controllers/system/systemController.js +254 -1
  245. package/server/controllers/system/systemMetricsController.js +2 -1
  246. package/server/controllers/userController.js +6 -0
  247. package/server/email/emailTemplateService.js +5 -3
  248. package/server/http/httpServer.js +11 -6
  249. package/server/middleware/rateLimit.js +2 -1
  250. package/server/middleware/securityHeaders.js +20 -1
  251. package/server/routes/admin/systemAdminRouter.js +6 -0
  252. package/server/routes/indexRouter.js +30 -6
  253. package/server/routes/observability/grafanaProxyRouter.js +254 -0
  254. package/server/routes/static/staticPageRouter.js +27 -1
  255. package/server/utils/publicContact.js +31 -0
  256. package/utils/time/timeModule.js +135 -0
  257. package/utils/time/timeModule.test.js +65 -0
  258. package/utils/whatsapp/contactEnv.js +39 -0
  259. package/vite.config.mjs +7 -1
  260. package/public/assets/images/brand-icon-192.png +0 -0
  261. package/scripts/sync-readme-snapshot.mjs +0 -133
@@ -29,9 +29,6 @@ RELEASE_GITHUB_RELEASE_INCLUDE_CHANGED_FILES="${RELEASE_GITHUB_RELEASE_INCLUDE_C
29
29
  RELEASE_GITHUB_RELEASE_MAX_FILES="${RELEASE_GITHUB_RELEASE_MAX_FILES:-300}"
30
30
  RELEASE_REQUIRE_DUAL_PUBLISH="${RELEASE_REQUIRE_DUAL_PUBLISH:-1}"
31
31
  RELEASE_VERIFY_UNIFIED_VERSION="${RELEASE_VERIFY_UNIFIED_VERSION:-1}"
32
- RELEASE_README_SYNC="${RELEASE_README_SYNC:-1}"
33
- RELEASE_README_SYNC_REQUIRED="${RELEASE_README_SYNC_REQUIRED:-0}"
34
- RELEASE_README_SYNC_COMMAND="${RELEASE_README_SYNC_COMMAND:-npm run readme:sync-snapshot}"
35
32
  RELEASE_WIKI_SYNC="${RELEASE_WIKI_SYNC:-1}"
36
33
  RELEASE_WIKI_SYNC_REQUIRED="${RELEASE_WIKI_SYNC_REQUIRED:-0}"
37
34
  RELEASE_WIKI_SYNC_COMMAND="${RELEASE_WIKI_SYNC_COMMAND:-bash ./scripts/wiki-sync.sh}"
@@ -480,20 +477,6 @@ if ! (
480
477
  exit 1
481
478
  fi
482
479
 
483
- if [ "$RELEASE_README_SYNC" = "1" ]; then
484
- log "Sincronizando bloco dinâmico do README"
485
- if ! (
486
- cd "$PROJECT_ROOT" &&
487
- bash -lc "$RELEASE_README_SYNC_COMMAND"
488
- ); then
489
- if [ "$RELEASE_README_SYNC_REQUIRED" = "1" ]; then
490
- printf '[release] Falha ao sincronizar README e RELEASE_README_SYNC_REQUIRED=1.\n' >&2
491
- exit 1
492
- fi
493
- log "Falha ao sincronizar README. Continuando release (RELEASE_README_SYNC_REQUIRED=0)."
494
- fi
495
- fi
496
-
497
480
  if [ "$RELEASE_WIKI_SYNC" = "1" ]; then
498
481
  log "Sincronizando wiki do GitHub"
499
482
  if ! (
@@ -563,11 +546,11 @@ if [ "$RELEASE_GITHUB_RELEASE" = "1" ]; then
563
546
  --tag "$release_tag" \
564
547
  --target "$local_target" \
565
548
  --name "$local_name" \
566
- --body-file "$release_body_file" \
567
549
  --generate-notes "$generate_notes_bool" \
568
550
  --prerelease "$prerelease_bool" \
569
551
  --draft "$draft_bool" \
570
- --latest "$latest_bool"
552
+ --latest "$latest_bool" \
553
+ --body-stdin "true" < "$release_body_file"
571
554
  )"
572
555
  else
573
556
  release_output="$(
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
2
3
 
3
4
  import fs from 'node:fs/promises';
4
5
  import path from 'node:path';
@@ -15,7 +16,7 @@ const MANUAL = 'MANUAL';
15
16
 
16
17
  const sqlErrorRegex = /(sql syntax|syntax error|mysql|sqlite|postgres|odbc|query failed|unclosed quotation|ORA-\d+)/i;
17
18
 
18
- const nowIso = () => new Date().toISOString();
19
+ const nowIso = () => __timeNowIso();
19
20
 
20
21
  const safeJson = async (value) => {
21
22
  try {
@@ -190,9 +191,9 @@ const testDdosSafe = async () => {
190
191
  const idx = cursor;
191
192
  cursor += 1;
192
193
  if (idx >= total) return;
193
- const startedAt = Date.now();
194
+ const startedAt = __timeNowMs();
194
195
  const res = await request('/healthz');
195
- const elapsed = Date.now() - startedAt;
196
+ const elapsed = __timeNowMs() - startedAt;
196
197
  latencies.push(elapsed);
197
198
  if (!res.ok) failures += 1;
198
199
  if (res.status >= 500) serverErrors += 1;
@@ -202,8 +203,8 @@ const testDdosSafe = async () => {
202
203
  await Promise.all(Array.from({ length: concurrency }, () => worker()));
203
204
  latencies.sort((a, b) => a - b);
204
205
  const p95 = latencies[Math.floor(latencies.length * 0.95)] || 0;
205
- const failureRate = total > 0 ? failures / total : 1;
206
- const serverErrorRate = total > 0 ? serverErrors / total : 1;
206
+ const failureRate = failures / total;
207
+ const serverErrorRate = serverErrors / total;
207
208
  const status = failureRate <= 0.1 && serverErrorRate === 0 ? PASS : WARN;
208
209
 
209
210
  return {
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { nowIso as __timeNowIso } from '#time';
4
+
5
+ import fs from 'node:fs/promises';
6
+ import path from 'node:path';
7
+ import process from 'node:process';
8
+
9
+ const rawBaseUrl = String(process.env.SECURITY_WEB_SURFACE_BASE_URL || 'https://omnizap.shop').trim() || 'https://omnizap.shop';
10
+ const reportPath = String(process.env.SECURITY_WEB_SURFACE_REPORT_PATH || './temp/security-web-surface-report.json').trim();
11
+ const requestTimeoutMs = Math.max(1_000, Number(process.env.SECURITY_WEB_SURFACE_TIMEOUT_MS || 10_000));
12
+
13
+ const toBaseOrigin = (value) => {
14
+ const raw = String(value || '').trim();
15
+ if (!raw) return 'https://omnizap.shop';
16
+ try {
17
+ const parsed = new URL(raw);
18
+ return parsed.origin;
19
+ } catch {
20
+ try {
21
+ const parsed = new URL(`https://${raw}`);
22
+ return parsed.origin;
23
+ } catch {
24
+ return 'https://omnizap.shop';
25
+ }
26
+ }
27
+ };
28
+
29
+ const baseOrigin = toBaseOrigin(rawBaseUrl);
30
+
31
+ const PASS = 'PASS';
32
+ const FAIL = 'FAIL';
33
+
34
+ const STATIC_REQUIRED_HEADERS = ['content-security-policy', 'permissions-policy', 'strict-transport-security', 'x-content-type-options', 'x-frame-options'];
35
+
36
+ const API_REQUIRED_HEADERS = ['content-security-policy', 'cross-origin-opener-policy', 'cross-origin-resource-policy', 'permissions-policy', 'strict-transport-security', 'x-content-type-options', 'x-frame-options'];
37
+
38
+ const checks = [
39
+ {
40
+ id: 1,
41
+ name: 'Root static page has security headers',
42
+ path: '/',
43
+ expectedStatuses: [200],
44
+ requiredHeaders: STATIC_REQUIRED_HEADERS,
45
+ },
46
+ {
47
+ id: 2,
48
+ name: 'Legal page has security headers',
49
+ path: '/notice-and-takedown/',
50
+ expectedStatuses: [200],
51
+ requiredHeaders: STATIC_REQUIRED_HEADERS,
52
+ },
53
+ {
54
+ id: 3,
55
+ name: 'Dotenv path is not exposed',
56
+ path: '/.env',
57
+ forbiddenStatuses: [200],
58
+ bodyLeakPatterns: [/DB_PASSWORD|MYSQL_PASSWORD|GITHUB_TOKEN|SECRET|PRIVATE_KEY/i],
59
+ },
60
+ {
61
+ id: 4,
62
+ name: 'Unknown path does not soft-fallback with 200',
63
+ path: '/__security_probe_nonexistent_omnizap__.txt',
64
+ expectedStatuses: [404],
65
+ },
66
+ {
67
+ id: 5,
68
+ name: 'Whitespace path fuzz does not return 200',
69
+ path: '/assets%20/',
70
+ forbiddenStatuses: [200],
71
+ },
72
+ {
73
+ id: 6,
74
+ name: 'API bootstrap keeps hardened headers',
75
+ path: '/api/home-bootstrap',
76
+ expectedStatuses: [200],
77
+ requiredHeaders: API_REQUIRED_HEADERS,
78
+ },
79
+ ];
80
+
81
+ const request = async (targetPath) => {
82
+ const normalizedPath = String(targetPath || '/').startsWith('/') ? String(targetPath || '/') : `/${String(targetPath || '/')}`;
83
+ const url = `${baseOrigin}${normalizedPath}`;
84
+ const controller = new AbortController();
85
+ const timeout = setTimeout(() => controller.abort(), requestTimeoutMs);
86
+
87
+ try {
88
+ const response = await fetch(url, {
89
+ method: 'GET',
90
+ redirect: 'manual',
91
+ signal: controller.signal,
92
+ });
93
+ const text = await response.text();
94
+ return {
95
+ ok: true,
96
+ url,
97
+ status: response.status,
98
+ headers: Object.fromEntries(response.headers.entries()),
99
+ body: text,
100
+ };
101
+ } catch (error) {
102
+ return {
103
+ ok: false,
104
+ url,
105
+ status: null,
106
+ headers: {},
107
+ body: '',
108
+ error: error?.message || String(error),
109
+ };
110
+ } finally {
111
+ clearTimeout(timeout);
112
+ }
113
+ };
114
+
115
+ const summarize = (items) =>
116
+ items.reduce(
117
+ (acc, item) => {
118
+ acc[item.status] = (acc[item.status] || 0) + 1;
119
+ return acc;
120
+ },
121
+ { PASS: 0, FAIL: 0 },
122
+ );
123
+
124
+ const runCheck = async (check) => {
125
+ const response = await request(check.path);
126
+ const reasons = [];
127
+
128
+ if (!response.ok) {
129
+ reasons.push(`network_error:${response.error || 'unknown'}`);
130
+ }
131
+
132
+ if (Array.isArray(check.expectedStatuses) && check.expectedStatuses.length > 0) {
133
+ if (!check.expectedStatuses.includes(response.status)) {
134
+ reasons.push(`unexpected_status:${response.status}`);
135
+ }
136
+ }
137
+
138
+ if (Array.isArray(check.forbiddenStatuses) && check.forbiddenStatuses.length > 0) {
139
+ if (check.forbiddenStatuses.includes(response.status)) {
140
+ reasons.push(`forbidden_status:${response.status}`);
141
+ }
142
+ }
143
+
144
+ const missingHeaders = [];
145
+ for (const headerName of check.requiredHeaders || []) {
146
+ const resolved = response.headers?.[headerName];
147
+ if (!resolved) missingHeaders.push(headerName);
148
+ }
149
+
150
+ if (missingHeaders.length > 0) {
151
+ reasons.push(`missing_headers:${missingHeaders.join(',')}`);
152
+ }
153
+
154
+ const leakPatternHit = (check.bodyLeakPatterns || []).find((pattern) => pattern.test(String(response.body || '')));
155
+ if (leakPatternHit) {
156
+ reasons.push(`body_leak_pattern:${String(leakPatternHit)}`);
157
+ }
158
+
159
+ return {
160
+ id: check.id,
161
+ name: check.name,
162
+ path: check.path,
163
+ status: reasons.length > 0 ? FAIL : PASS,
164
+ reasons,
165
+ evidence: {
166
+ url: response.url,
167
+ status: response.status,
168
+ headers: response.headers,
169
+ body_preview: String(response.body || '').slice(0, 240),
170
+ },
171
+ };
172
+ };
173
+
174
+ const writeReport = async (payload) => {
175
+ const absolutePath = path.resolve(process.cwd(), reportPath);
176
+ await fs.mkdir(path.dirname(absolutePath), { recursive: true });
177
+ await fs.writeFile(absolutePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
178
+ return absolutePath;
179
+ };
180
+
181
+ const main = async () => {
182
+ const startedAt = __timeNowIso();
183
+ const results = [];
184
+
185
+ for (const check of checks) {
186
+ const result = await runCheck(check);
187
+ results.push(result);
188
+ console.log(`[security-web-surface] ${String(result.id).padStart(2, '0')} ${result.name}: ${result.status}`);
189
+ }
190
+
191
+ const summary = summarize(results);
192
+ const endedAt = __timeNowIso();
193
+ const report = {
194
+ meta: {
195
+ base_origin: baseOrigin,
196
+ started_at: startedAt,
197
+ ended_at: endedAt,
198
+ timeout_ms: requestTimeoutMs,
199
+ },
200
+ summary,
201
+ results,
202
+ };
203
+
204
+ const reportAbsolutePath = await writeReport(report);
205
+ console.log('[security-web-surface] ---');
206
+ console.log(`[security-web-surface] base_origin=${baseOrigin}`);
207
+ console.log(`[security-web-surface] summary=${JSON.stringify(summary)}`);
208
+ console.log(`[security-web-surface] report_path=${reportAbsolutePath}`);
209
+
210
+ if ((summary.FAIL || 0) > 0) {
211
+ process.exitCode = 1;
212
+ }
213
+ };
214
+
215
+ main().catch((error) => {
216
+ console.error(`[security-web-surface] fatal_error=${error?.message || error}`);
217
+ process.exit(1);
218
+ });
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
2
3
  import http from 'node:http';
3
4
  import https from 'node:https';
4
5
  import { performance } from 'node:perf_hooks';
@@ -106,7 +107,7 @@ const runRequest = (pathName) =>
106
107
 
107
108
  const runWorker = async ({ deadlineMs, workerIndex, stats }) => {
108
109
  let requestIndex = workerIndex % requestPaths.length;
109
- while (Date.now() < deadlineMs) {
110
+ while (__timeNowMs() < deadlineMs) {
110
111
  const pathName = requestPaths[requestIndex % requestPaths.length];
111
112
  requestIndex += 1;
112
113
 
@@ -129,7 +130,7 @@ const runWorker = async ({ deadlineMs, workerIndex, stats }) => {
129
130
  };
130
131
 
131
132
  const main = async () => {
132
- const startedAt = Date.now();
133
+ const startedAt = __timeNowMs();
133
134
  const deadlineMs = startedAt + durationSeconds * 1000;
134
135
  const stats = {
135
136
  total: 0,
@@ -154,7 +155,7 @@ const main = async () => {
154
155
  ),
155
156
  );
156
157
 
157
- const elapsedSeconds = Math.max(0.001, (Date.now() - startedAt) / 1000);
158
+ const elapsedSeconds = Math.max(0.001, (__timeNowMs() - startedAt) / 1000);
158
159
  const sortedLatencies = [...stats.latencies].sort((a, b) => a - b);
159
160
  const p50 = quantile(sortedLatencies, 0.5);
160
161
  const p90 = quantile(sortedLatencies, 0.9);
@@ -165,7 +166,7 @@ const main = async () => {
165
166
 
166
167
  const summary = {
167
168
  started_at: new Date(startedAt).toISOString(),
168
- ended_at: new Date().toISOString(),
169
+ ended_at: __timeNowIso(),
169
170
  base_url: baseUrl,
170
171
  duration_seconds: Number(elapsedSeconds.toFixed(3)),
171
172
  concurrency,
@@ -1,3 +1,4 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
1
2
  import { createHash, randomUUID } from 'node:crypto';
2
3
  import axios from 'axios';
3
4
 
@@ -35,7 +36,7 @@ export const createGoogleWebAuthService = ({ executeQuery, runSqlTransaction, ta
35
36
  const USER_PASSWORD_TABLE = String(tables.STICKER_WEB_USER_PASSWORD || 'web_user_password').trim() || 'web_user_password';
36
37
 
37
38
  const pruneExpiredGoogleSessions = () => {
38
- const now = Date.now();
39
+ const now = __timeNowMs();
39
40
  for (const [token, session] of webGoogleSessionMap.entries()) {
40
41
  if (!session || Number(session.expiresAt || 0) <= now) {
41
42
  webGoogleSessionMap.delete(token);
@@ -145,15 +146,15 @@ export const createGoogleWebAuthService = ({ executeQuery, runSqlTransaction, ta
145
146
  picture: String(row.picture_url || '').trim() || null,
146
147
  ownerJid,
147
148
  ownerPhone,
148
- createdAt: Number.isFinite(createdAtRaw) ? createdAtRaw : Date.now(),
149
+ createdAt: Number.isFinite(createdAtRaw) ? createdAtRaw : __timeNowMs(),
149
150
  expiresAt,
150
151
  lastSeenAt: Number.isFinite(lastSeenAtRaw) ? lastSeenAtRaw : 0,
151
- lastDbTouchAt: Date.now(),
152
+ lastDbTouchAt: __timeNowMs(),
152
153
  };
153
154
  };
154
155
 
155
156
  const maybePruneExpiredGoogleSessionsFromDb = async () => {
156
- const now = Date.now();
157
+ const now = __timeNowMs();
157
158
  if (now - googleWebSessionDbPruneAt < sessionDbPruneIntervalMs) return;
158
159
  googleWebSessionDbPruneAt = now;
159
160
  try {
@@ -368,7 +369,7 @@ export const createGoogleWebAuthService = ({ executeQuery, runSqlTransaction, ta
368
369
  const createGoogleWebSession = (claims, { ownerJid } = {}) => {
369
370
  pruneExpiredGoogleSessions();
370
371
  const token = randomUUID();
371
- const now = Date.now();
372
+ const now = __timeNowMs();
372
373
  const resolvedOwnerJid = normalizeJid(ownerJid) || buildGoogleOwnerJid(claims.sub);
373
374
  const resolvedOwnerPhone = toWhatsAppPhoneDigits(resolvedOwnerJid) || '';
374
375
  return {
@@ -394,7 +395,7 @@ export const createGoogleWebAuthService = ({ executeQuery, runSqlTransaction, ta
394
395
 
395
396
  const issueAccessTokenForSession = (session) => {
396
397
  if (!session?.sub) return '';
397
- const expiresInSeconds = Math.max(60, Math.floor((Number(session.expiresAt || 0) - Date.now()) / 1000));
398
+ const expiresInSeconds = Math.max(60, Math.floor((Number(session.expiresAt || 0) - __timeNowMs()) / 1000));
398
399
  return (
399
400
  signWebAuthJwt(
400
401
  {
@@ -495,7 +496,7 @@ export const createGoogleWebAuthService = ({ executeQuery, runSqlTransaction, ta
495
496
 
496
497
  const touchGoogleWebSessionActivity = (session) => {
497
498
  if (!session?.token || !session?.sub) return;
498
- const now = Date.now();
499
+ const now = __timeNowMs();
499
500
  session.lastSeenAt = now;
500
501
  if (now - Number(session.lastDbTouchAt || 0) < sessionDbTouchIntervalMs) {
501
502
  return;
@@ -15,7 +15,7 @@ const clampInt = (value, fallback, min, max) => {
15
15
  };
16
16
 
17
17
  const WEB_AUTH_JWT_SECRET = String(process.env.WEB_AUTH_JWT_SECRET || '').trim();
18
- const WEB_AUTH_JWT_ISSUER = String(process.env.WEB_AUTH_JWT_ISSUER || 'omnizap-system').trim() || 'omnizap-system';
18
+ const WEB_AUTH_JWT_ISSUER = String(process.env.WEB_AUTH_JWT_ISSUER || 'omnizap').trim() || 'omnizap';
19
19
  const WEB_AUTH_JWT_AUDIENCE = String(process.env.WEB_AUTH_JWT_AUDIENCE || 'omnizap-web').trim() || 'omnizap-web';
20
20
  const WEB_AUTH_JWT_EXPIRES_IN = String(process.env.WEB_AUTH_JWT_EXPIRES_IN || '7d').trim() || '7d';
21
21
  const WEB_AUTH_JWT_DISABLED = parseEnvBool(process.env.WEB_AUTH_JWT_DISABLED, false);
@@ -1,3 +1,4 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
1
2
  import { createGoogleWebAuthRuntime, normalizeGoogleSubject } from './googleWebAuth/googleWebAuthRuntime.js';
2
3
  import { isWebAuthJwtEnabled, signWebAuthJwt, verifyWebAuthJwt } from './jwt/webJwtService.js';
3
4
  import userPasswordAuthService from './userPassword/index.js';
@@ -13,7 +14,7 @@ export const createStickerCatalogAuthContext = ({ executeQuery, runSqlTransactio
13
14
 
14
15
  const safeName = sanitizeText(name || '', 80, { allowEmpty: true }) || '';
15
16
  const normalizedOwnerJid = normalizeJid(ownerJid) || null;
16
- const idempotencyKey = `google_web_welcome:${normalizedEmail}:${new Date().toISOString().slice(0, 10)}`;
17
+ const idempotencyKey = `google_web_welcome:${normalizedEmail}:${__timeNowIso().slice(0, 10)}`;
17
18
 
18
19
  void queueWelcomeEmail({
19
20
  to: normalizedEmail,
@@ -1,3 +1,4 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
1
2
  import { createHash, randomUUID } from 'node:crypto';
2
3
 
3
4
  const DEFAULT_LEGAL_VERSION = '2026-03-07';
@@ -129,7 +130,7 @@ export const createTermsAcceptanceHandler = ({ executeQuery, tables, logger, sen
129
130
  }
130
131
 
131
132
  const source = normalizeTermsAcceptanceSource(payload.source) || legalDefaultAcceptanceSource;
132
- const acceptedAt = new Date();
133
+ const acceptedAt = __timeNow();
133
134
  const acceptedAtClient = parseAcceptedAtClientDate(payload.accepted_at);
134
135
  const cookies = parseCookies(req);
135
136
  const sessionKey = normalizeTermsAcceptanceSessionKey(cookies[webSessionCookieName] || '');
@@ -1,3 +1,4 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
1
2
  import { hashUserPassword, resolveUserPasswordPolicy, validateUserPassword, verifyUserPasswordHash } from './userPasswordCrypto.js';
2
3
 
3
4
  const clampInt = (value, fallback, min, max) => {
@@ -139,7 +140,7 @@ export const createUserPasswordAuthService = ({ executeQuery, tables = {}, logge
139
140
  return { locked: false, retryAfterSeconds: 0 };
140
141
  }
141
142
 
142
- const elapsedSeconds = Math.max(0, Math.floor((Date.now() - lastFailedAtMs) / 1000));
143
+ const elapsedSeconds = Math.max(0, Math.floor((__timeNowMs() - lastFailedAtMs) / 1000));
143
144
  const retryAfterSeconds = Math.max(0, lockoutSeconds - elapsedSeconds);
144
145
  if (retryAfterSeconds <= 0) {
145
146
  return { locked: false, retryAfterSeconds: 0 };
@@ -1,3 +1,4 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
1
2
  import { createHash, pbkdf2 as pbkdf2Callback, randomInt, randomUUID, timingSafeEqual } from 'node:crypto';
2
3
  import { promisify } from 'node:util';
3
4
 
@@ -312,7 +313,7 @@ export const createUserPasswordRecoveryService = ({ executeQuery, userPasswordAu
312
313
  const oldestCreatedAt = rows?.[0]?.oldest_created_at ? Date.parse(rows[0].oldest_created_at) : NaN;
313
314
  if (!Number.isFinite(oldestCreatedAt)) return safeWindowSeconds;
314
315
 
315
- const elapsedSeconds = Math.max(0, Math.floor((Date.now() - oldestCreatedAt) / 1000));
316
+ const elapsedSeconds = Math.max(0, Math.floor((__timeNowMs() - oldestCreatedAt) / 1000));
316
317
  return Math.max(1, safeWindowSeconds - elapsedSeconds);
317
318
  };
318
319
 
@@ -334,7 +335,7 @@ export const createUserPasswordRecoveryService = ({ executeQuery, userPasswordAu
334
335
  const row = Array.isArray(rows) ? rows[0] : null;
335
336
  if (!row) return null;
336
337
  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 elapsedSeconds = Number.isFinite(createdAtMs) ? Math.max(0, Math.floor((__timeNowMs() - createdAtMs) / 1000)) : resendCooldownSeconds;
338
339
  const retryAfterSeconds = Math.max(0, resendCooldownSeconds - elapsedSeconds);
339
340
  return {
340
341
  id: Number(row.id || 0),
@@ -496,7 +497,7 @@ export const createUserPasswordRecoveryService = ({ executeQuery, userPasswordAu
496
497
  remote_ip: normalizeIp(requestMeta?.remoteIp) || null,
497
498
  },
498
499
  priority: 95,
499
- idempotencyKey: `web_user_password_recovery:${knownUser.google_sub}:${normalizedPurpose}:${new Date().toISOString().slice(0, 16)}`,
500
+ idempotencyKey: `web_user_password_recovery:${knownUser.google_sub}:${normalizedPurpose}:${__timeNowIso().slice(0, 16)}`,
500
501
  });
501
502
  } catch (error) {
502
503
  await executeQuery(
@@ -1,3 +1,4 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
1
2
  import { scryptSync } from 'node:crypto';
2
3
 
3
4
  const MY_PROFILE_DEFAULT_STATS = Object.freeze({
@@ -119,7 +120,7 @@ const toPasswordRecoverySessionExpiresAt = (claims) => {
119
120
  const toPasswordRecoverySessionExpiresIn = (claims) => {
120
121
  const expUnix = Number(claims?.exp || 0);
121
122
  if (!Number.isFinite(expUnix) || expUnix <= 0) return null;
122
- return Math.max(0, Math.floor(expUnix - Date.now() / 1000));
123
+ return Math.max(0, Math.floor(expUnix - __timeNowMs() / 1000));
123
124
  };
124
125
 
125
126
  const signPasswordRecoverySessionToken = ({ sub = '', email = '', ownerJid = '' } = {}, { isWebAuthJwtEnabled, signWebAuthJwt, passwordRecoverySessionAuthMethod, passwordRecoverySessionTtlSeconds }) => {
@@ -202,9 +203,7 @@ const isUnknownColumnError = (error, columnName = '') => {
202
203
  if (code !== 'ER_BAD_FIELD_ERROR' && errno !== 1054) return false;
203
204
  if (!columnName) return true;
204
205
  const message = String(error?.message || '').toLowerCase();
205
- const normalizedColumn = String(columnName || '')
206
- .trim()
207
- .toLowerCase();
206
+ const normalizedColumn = String(columnName).trim().toLowerCase();
208
207
  if (!normalizedColumn) return true;
209
208
  return message.includes(`unknown column '${normalizedColumn}'`) || message.includes(`unknown column \`${normalizedColumn}\``);
210
209
  };
@@ -233,7 +232,7 @@ export const createWebAccountAuthHandlers = ({ sendJson, readJsonBody, logger, p
233
232
  return '';
234
233
  };
235
234
 
236
- const maybePrunePasswordLoginIdentityThrottle = async (nowMs = Date.now()) => {
235
+ const maybePrunePasswordLoginIdentityThrottle = async (nowMs = __timeNowMs()) => {
237
236
  if (nowMs - passwordLoginIdentityPruneAt < 60 * 60 * 1000) return;
238
237
  passwordLoginIdentityPruneAt = nowMs;
239
238
  const staleAfterSeconds = Math.max(60 * 60, passwordLoginIdentityLockoutSeconds * 2);
@@ -266,12 +265,12 @@ export const createWebAccountAuthHandlers = ({ sendJson, readJsonBody, logger, p
266
265
  );
267
266
  const row = Array.isArray(rows) ? rows[0] : null;
268
267
  const lockedUntilMs = Date.parse(String(row?.locked_until || ''));
269
- if (!Number.isFinite(lockedUntilMs) || lockedUntilMs <= Date.now()) {
268
+ if (!Number.isFinite(lockedUntilMs) || lockedUntilMs <= __timeNowMs()) {
270
269
  return { locked: false, retryAfterSeconds: 0 };
271
270
  }
272
271
  return {
273
272
  locked: true,
274
- retryAfterSeconds: Math.max(1, Math.ceil((lockedUntilMs - Date.now()) / 1000)),
273
+ retryAfterSeconds: Math.max(1, Math.ceil((lockedUntilMs - __timeNowMs()) / 1000)),
275
274
  };
276
275
  } catch (error) {
277
276
  logger?.warn?.('Falha ao consultar throttle distribuido de login por identidade.', {
@@ -321,12 +320,12 @@ export const createWebAccountAuthHandlers = ({ sendJson, readJsonBody, logger, p
321
320
  );
322
321
  const row = Array.isArray(rows) ? rows[0] : null;
323
322
  const lockedUntilMs = Date.parse(String(row?.locked_until || ''));
324
- if (!Number.isFinite(lockedUntilMs) || lockedUntilMs <= Date.now()) {
323
+ if (!Number.isFinite(lockedUntilMs) || lockedUntilMs <= __timeNowMs()) {
325
324
  return { locked: false, retryAfterSeconds: 0 };
326
325
  }
327
326
  return {
328
327
  locked: true,
329
- retryAfterSeconds: Math.max(1, Math.ceil((lockedUntilMs - Date.now()) / 1000)),
328
+ retryAfterSeconds: Math.max(1, Math.ceil((lockedUntilMs - __timeNowMs()) / 1000)),
330
329
  };
331
330
  } catch (error) {
332
331
  logger?.warn?.('Falha ao registrar tentativa no throttle distribuido de login por identidade.', {
@@ -1201,7 +1200,7 @@ export const createWebAccountAuthHandlers = ({ sendJson, readJsonBody, logger, p
1201
1200
 
1202
1201
  let avgDaily = 0;
1203
1202
  if (usage.messages > 0 && usage.first_message_at) {
1204
- const daysDiff = Math.max(1, (Date.now() - new Date(usage.first_message_at).getTime()) / (1000 * 60 * 60 * 24));
1203
+ const daysDiff = Math.max(1, (__timeNowMs() - new Date(usage.first_message_at).getTime()) / (1000 * 60 * 60 * 24));
1205
1204
  avgDaily = (usage.messages / daysDiff).toFixed(2);
1206
1205
  }
1207
1206