@omnizap-system/omnizap 2.6.1 → 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 (156) hide show
  1. package/.env.example +54 -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 +2 -0
  6. package/app/configParts/adminIdentity.js +5 -5
  7. package/app/configParts/baileysConfig.js +226 -55
  8. package/app/configParts/groupUtils.js +5 -0
  9. package/app/configParts/messagePersistenceService.js +143 -3
  10. package/app/configParts/sessionConfig.js +157 -0
  11. package/app/connection/baileysCompatibility.test.js +1 -1
  12. package/app/connection/groupOwnerWriteStateResolver.js +109 -0
  13. package/app/connection/socketController.js +625 -124
  14. package/app/connection/socketController.multiSession.test.js +108 -0
  15. package/app/controllers/messageController.js +1 -1
  16. package/app/controllers/messagePipeline/commandMiddleware.js +12 -10
  17. package/app/controllers/messagePipeline/conversationMiddleware.js +2 -1
  18. package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +104 -0
  19. package/app/controllers/messagePipeline/preProcessingMiddlewares.js +80 -2
  20. package/app/controllers/messageProcessingPipeline.js +88 -9
  21. package/app/controllers/messageProcessingPipeline.test.js +200 -0
  22. package/app/modules/adminModule/AGENT.md +1 -1
  23. package/app/modules/adminModule/commandConfig.json +3318 -1347
  24. package/app/modules/adminModule/groupCommandHandlers.js +856 -14
  25. package/app/modules/adminModule/groupCommandHandlers.test.js +375 -9
  26. package/app/modules/adminModule/groupWarningRepository.js +152 -0
  27. package/app/modules/aiModule/AGENT.md +47 -30
  28. package/app/modules/aiModule/aiConfigRuntime.js +1 -0
  29. package/app/modules/aiModule/catCommand.js +132 -25
  30. package/app/modules/aiModule/commandConfig.json +114 -28
  31. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +54 -6
  32. package/app/modules/gameModule/AGENT.md +1 -1
  33. package/app/modules/gameModule/commandConfig.json +29 -0
  34. package/app/modules/menuModule/AGENT.md +1 -1
  35. package/app/modules/menuModule/commandConfig.json +45 -10
  36. package/app/modules/menuModule/menuCatalogService.js +190 -0
  37. package/app/modules/menuModule/menuCommandUsageRepository.js +109 -0
  38. package/app/modules/menuModule/menuDynamicService.js +511 -0
  39. package/app/modules/menuModule/menuDynamicService.test.js +141 -0
  40. package/app/modules/menuModule/menus.js +36 -5
  41. package/app/modules/playModule/AGENT.md +10 -5
  42. package/app/modules/playModule/commandConfig.json +74 -16
  43. package/app/modules/playModule/playCommandConstants.js +13 -7
  44. package/app/modules/playModule/playCommandCore.js +4 -6
  45. package/app/modules/playModule/{playCommandYtDlpClient.js → playCommandMediaClient.js} +684 -332
  46. package/app/modules/playModule/playConfigRuntime.js +5 -6
  47. package/app/modules/playModule/playModuleCriticalFlows.test.js +44 -59
  48. package/app/modules/quoteModule/AGENT.md +1 -1
  49. package/app/modules/quoteModule/commandConfig.json +29 -0
  50. package/app/modules/rpgPokemonModule/AGENT.md +1 -1
  51. package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
  52. package/app/modules/statsModule/AGENT.md +1 -1
  53. package/app/modules/statsModule/commandConfig.json +58 -0
  54. package/app/modules/stickerModule/AGENT.md +1 -1
  55. package/app/modules/stickerModule/commandConfig.json +145 -0
  56. package/app/modules/stickerPackModule/AGENT.md +1 -1
  57. package/app/modules/stickerPackModule/autoPackCollectorService.js +5 -1
  58. package/app/modules/stickerPackModule/commandConfig.json +29 -0
  59. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +1 -1
  60. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +78 -57
  61. package/app/modules/stickerPackModule/stickerPackService.js +13 -6
  62. package/app/modules/systemMetricsModule/AGENT.md +1 -1
  63. package/app/modules/systemMetricsModule/commandConfig.json +29 -0
  64. package/app/modules/tiktokModule/AGENT.md +1 -1
  65. package/app/modules/tiktokModule/commandConfig.json +29 -0
  66. package/app/modules/userModule/AGENT.md +1 -1
  67. package/app/modules/userModule/commandConfig.json +29 -0
  68. package/app/modules/waifuPicsModule/AGENT.md +57 -27
  69. package/app/modules/waifuPicsModule/commandConfig.json +87 -0
  70. package/app/observability/metrics.js +136 -0
  71. package/app/services/ai/commandConfigEnrichmentService.js +229 -47
  72. package/app/services/ai/geminiService.js +131 -7
  73. package/app/services/ai/geminiService.test.js +59 -2
  74. package/app/services/ai/moduleAiHelpCoreService.js +33 -4
  75. package/app/services/group/groupMetadataService.js +24 -1
  76. package/app/services/infra/dbWriteQueue.js +51 -21
  77. package/app/services/messaging/newsBroadcastService.js +843 -27
  78. package/app/services/multiSession/assignmentBalancerService.js +457 -0
  79. package/app/services/multiSession/groupOwnershipRepository.js +381 -0
  80. package/app/services/multiSession/groupOwnershipService.js +890 -0
  81. package/app/services/multiSession/groupOwnershipService.test.js +309 -0
  82. package/app/services/multiSession/sessionRegistryService.js +293 -0
  83. package/app/store/aiPromptStore.js +36 -19
  84. package/app/store/groupConfigStore.js +41 -5
  85. package/app/store/premiumUserStore.js +21 -7
  86. package/app/utils/antiLink/antiLinkModule.js +352 -16
  87. package/app/workers/aiHelperContinuousLearningWorker.js +512 -0
  88. package/database/index.js +6 -0
  89. package/database/migrations/20260307_d0_hardening_down.sql +1 -1
  90. package/database/migrations/20260314_d7_canonical_sender_down.sql +1 -1
  91. package/database/migrations/20260406_d30_security_analytics_down.sql +1 -1
  92. package/database/migrations/20260411_d35_group_community_metadata_down.sql +59 -0
  93. package/database/migrations/20260411_d35_group_community_metadata_up.sql +62 -0
  94. package/database/migrations/20260412_d36_system_config_tables_down.sql +32 -0
  95. package/database/migrations/20260412_d36_system_config_tables_up.sql +66 -0
  96. package/database/migrations/20260413_d37_group_user_warnings_down.sql +11 -0
  97. package/database/migrations/20260413_d37_group_user_warnings_up.sql +24 -0
  98. package/database/migrations/20260414_d38_multi_session_foundation_down.sql +72 -0
  99. package/database/migrations/20260414_d38_multi_session_foundation_up.sql +125 -0
  100. package/database/migrations/20260414_d39_multi_session_cutover_down.sql +103 -0
  101. package/database/migrations/20260414_d39_multi_session_cutover_up.sql +83 -0
  102. package/database/schema.sql +102 -1
  103. package/docker-compose.yml +4 -1
  104. package/docs/compliance/acceptable-use-policy-2026-03-07.md +1 -1
  105. package/docs/compliance/privacy-policy-2026-03-07.md +2 -2
  106. package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
  107. package/docs/security/network-hardening-runbook-2026-03-07.md +53 -0
  108. package/docs/security/omnizap-static-security-headers.conf +25 -0
  109. package/ecosystem.prod.config.cjs +31 -11
  110. package/index.js +52 -18
  111. package/observability/alert-rules.yml +20 -0
  112. package/observability/grafana/dashboards/omnizap-system-admin.json +229 -0
  113. package/observability/mysql-setup.sql +4 -4
  114. package/observability/system-admin-observability.md +26 -0
  115. package/package.json +12 -5
  116. package/public/comandos/commands-catalog.json +2253 -78
  117. package/public/js/apps/commandsReactApp.js +267 -87
  118. package/public/js/apps/createPackApp.js +3 -3
  119. package/public/js/apps/stickersApp.js +255 -103
  120. package/public/js/apps/termsReactApp.js +57 -8
  121. package/public/js/apps/userPasswordResetReactApp.js +406 -0
  122. package/public/js/apps/userReactApp.js +96 -47
  123. package/public/js/apps/userSystemAdmReactApp.js +1506 -0
  124. package/public/pages/politica-de-privacidade.html +1 -1
  125. package/public/pages/stickers.html +5 -5
  126. package/public/pages/termos-de-uso-texto-integral.html +1 -1
  127. package/public/pages/termos-de-uso.html +1 -1
  128. package/public/pages/user-password-reset.html +3 -4
  129. package/public/pages/user-systemadm.html +8 -462
  130. package/public/pages/user.html +1 -1
  131. package/scripts/clear-whatsapp-session.sh +123 -0
  132. package/scripts/core-ai-mode.mjs +163 -0
  133. package/scripts/deploy.sh +10 -0
  134. package/scripts/enrich-command-config-ux-openai.mjs +492 -0
  135. package/scripts/generate-commands-catalog.mjs +155 -0
  136. package/scripts/new-whatsapp-session.sh +317 -0
  137. package/scripts/security-web-surface-check.mjs +218 -0
  138. package/server/controllers/admin/adminPanelHandlers.js +253 -3
  139. package/server/controllers/admin/systemAdminController.js +267 -0
  140. package/server/controllers/sticker/stickerCatalogController.js +9 -23
  141. package/server/controllers/system/contactController.js +9 -17
  142. package/server/controllers/system/stickerCatalogSystemContext.js +27 -6
  143. package/server/controllers/system/systemController.js +254 -1
  144. package/server/controllers/userController.js +6 -0
  145. package/server/email/emailTemplateService.js +3 -2
  146. package/server/http/httpServer.js +8 -4
  147. package/server/middleware/securityHeaders.js +20 -1
  148. package/server/routes/admin/systemAdminRouter.js +6 -0
  149. package/server/routes/indexRouter.js +30 -6
  150. package/server/routes/observability/grafanaProxyRouter.js +254 -0
  151. package/server/routes/static/staticPageRouter.js +27 -1
  152. package/server/utils/publicContact.js +31 -0
  153. package/utils/whatsapp/contactEnv.js +39 -0
  154. package/vite.config.mjs +2 -1
  155. package/app/modules/playModule/local/installYtDlp.js +0 -25
  156. package/app/modules/playModule/local/ytDlpInstaller.js +0 -28
@@ -0,0 +1,317 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ ENV_FILE="${ENV_FILE:-$PROJECT_ROOT/.env}"
6
+
7
+ SESSION_ID_PATTERN='^[a-zA-Z0-9:_-]+$'
8
+ SESSION_ID_MAX_LENGTH=64
9
+
10
+ SESSION_ID=""
11
+ SESSION_PREFIX="work"
12
+ SESSION_WEIGHT=1
13
+ SET_PRIMARY=0
14
+ CONNECT_NOW=1
15
+ RESET_AUTH=0
16
+ CLEAR_AUTH_FILES=0
17
+ ALLOW_REUSE=0
18
+
19
+ log() {
20
+ printf '[new-whatsapp-session] %s\n' "$*"
21
+ }
22
+
23
+ fail() {
24
+ printf '[new-whatsapp-session] erro: %s\n' "$*" >&2
25
+ exit 1
26
+ }
27
+
28
+ require_cmd() {
29
+ if ! command -v "$1" >/dev/null 2>&1; then
30
+ fail "comando ausente: $1"
31
+ fi
32
+ }
33
+
34
+ usage() {
35
+ cat <<'EOF'
36
+ Uso:
37
+ bash scripts/new-whatsapp-session.sh [opcoes]
38
+
39
+ Opcoes:
40
+ --session <id> Define o session_id manualmente.
41
+ --prefix <valor> Prefixo para gerar session_id automatico (padrao: work).
42
+ --weight <1-1000> Peso da sessao em BAILEYS_SESSION_WEIGHTS (padrao: 1).
43
+ --primary Define a nova sessao como BAILEYS_PRIMARY_SESSION_ID.
44
+ --reuse Permite usar um session_id ja existente.
45
+ --reset-auth Limpa credenciais atuais da sessao no MySQL antes do QR.
46
+ --clear-auth-files Junto com --reset-auth, remove app/connection/auth/*.json.
47
+ --no-connect Apenas atualiza .env (nao abre conexao para QR agora).
48
+ --help Mostra esta ajuda.
49
+
50
+ Exemplos:
51
+ npm run new:work
52
+ npm run new:work -- --session suporte_2 --primary
53
+ bash scripts/new-whatsapp-session.sh --prefix operador --no-connect
54
+ EOF
55
+ }
56
+
57
+ strip_wrapping_quotes() {
58
+ local value="${1:-}"
59
+ if [[ "$value" == \"*\" && "$value" == *\" ]]; then
60
+ value="${value:1:${#value}-2}"
61
+ elif [[ "$value" == \'*\' && "$value" == *\' ]]; then
62
+ value="${value:1:${#value}-2}"
63
+ fi
64
+ printf '%s' "$value"
65
+ }
66
+
67
+ read_env_value() {
68
+ local key="$1"
69
+ local raw=""
70
+
71
+ if [[ -f "$ENV_FILE" ]]; then
72
+ raw="$(grep -E "^${key}=" "$ENV_FILE" | tail -n 1 | cut -d'=' -f2- || true)"
73
+ fi
74
+
75
+ raw="${raw//$'\r'/}"
76
+ strip_wrapping_quotes "$raw"
77
+ }
78
+
79
+ split_entries() {
80
+ printf '%s' "${1:-}" | tr ',;\n' '\n' | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//' | awk 'NF'
81
+ }
82
+
83
+ validate_session_id() {
84
+ local value="$1"
85
+ [[ -n "$value" ]] || return 1
86
+ [[ "${#value}" -le "$SESSION_ID_MAX_LENGTH" ]] || return 1
87
+ [[ "$value" =~ $SESSION_ID_PATTERN ]] || return 1
88
+ return 0
89
+ }
90
+
91
+ upsert_env_value() {
92
+ local key="$1"
93
+ local value="$2"
94
+ local temp_file
95
+ temp_file="$(mktemp)"
96
+
97
+ awk -v key="$key" -v value="$value" '
98
+ BEGIN {
99
+ replaced = 0;
100
+ prefix = key "=";
101
+ }
102
+ index($0, prefix) == 1 {
103
+ if (replaced == 0) {
104
+ print prefix value;
105
+ replaced = 1;
106
+ }
107
+ next;
108
+ }
109
+ { print; }
110
+ END {
111
+ if (replaced == 0) {
112
+ print prefix value;
113
+ }
114
+ }
115
+ ' "$ENV_FILE" >"$temp_file"
116
+
117
+ mv "$temp_file" "$ENV_FILE"
118
+ }
119
+
120
+ is_valid_weight() {
121
+ local value="$1"
122
+ [[ "$value" =~ ^[0-9]+$ ]] || return 1
123
+ ((value >= 1 && value <= 1000)) || return 1
124
+ return 0
125
+ }
126
+
127
+ while [[ $# -gt 0 ]]; do
128
+ case "$1" in
129
+ --session)
130
+ [[ $# -ge 2 ]] || fail "faltou valor para --session"
131
+ SESSION_ID="$2"
132
+ shift 2
133
+ ;;
134
+ --prefix)
135
+ [[ $# -ge 2 ]] || fail "faltou valor para --prefix"
136
+ SESSION_PREFIX="$2"
137
+ shift 2
138
+ ;;
139
+ --weight)
140
+ [[ $# -ge 2 ]] || fail "faltou valor para --weight"
141
+ SESSION_WEIGHT="$2"
142
+ shift 2
143
+ ;;
144
+ --primary)
145
+ SET_PRIMARY=1
146
+ shift
147
+ ;;
148
+ --reuse)
149
+ ALLOW_REUSE=1
150
+ shift
151
+ ;;
152
+ --reset-auth)
153
+ RESET_AUTH=1
154
+ shift
155
+ ;;
156
+ --clear-auth-files)
157
+ CLEAR_AUTH_FILES=1
158
+ shift
159
+ ;;
160
+ --no-connect)
161
+ CONNECT_NOW=0
162
+ shift
163
+ ;;
164
+ -h|--help)
165
+ usage
166
+ exit 0
167
+ ;;
168
+ *)
169
+ fail "opcao invalida: $1 (use --help)"
170
+ ;;
171
+ esac
172
+ done
173
+
174
+ [[ -f "$ENV_FILE" ]] || fail "arquivo .env nao encontrado em: $ENV_FILE"
175
+ is_valid_weight "$SESSION_WEIGHT" || fail "peso invalido em --weight: $SESSION_WEIGHT (use 1..1000)"
176
+
177
+ if [[ -n "$SESSION_ID" ]]; then
178
+ validate_session_id "$SESSION_ID" || fail "session_id invalido: \"$SESSION_ID\""
179
+ else
180
+ validate_session_id "$SESSION_PREFIX" || fail "prefixo invalido para session_id: \"$SESSION_PREFIX\""
181
+ fi
182
+
183
+ declare -A SEEN_SESSION_IDS=()
184
+ declare -a SESSION_IDS=()
185
+
186
+ existing_session_ids_raw="$(read_env_value BAILEYS_SESSION_IDS)"
187
+ legacy_session_id_raw="$(read_env_value BAILEYS_AUTH_SESSION_ID)"
188
+ if [[ -z "$existing_session_ids_raw" ]]; then
189
+ existing_session_ids_raw="$legacy_session_id_raw"
190
+ fi
191
+ if [[ -z "$existing_session_ids_raw" ]]; then
192
+ existing_session_ids_raw="default"
193
+ fi
194
+
195
+ while IFS= read -r candidate; do
196
+ [[ -n "$candidate" ]] || continue
197
+ if ! validate_session_id "$candidate"; then
198
+ log "ignorado session_id invalido no .env: $candidate"
199
+ continue
200
+ fi
201
+ if [[ -z "${SEEN_SESSION_IDS[$candidate]+x}" ]]; then
202
+ SEEN_SESSION_IDS["$candidate"]=1
203
+ SESSION_IDS+=("$candidate")
204
+ fi
205
+ done < <(split_entries "$existing_session_ids_raw")
206
+
207
+ if [[ "${#SESSION_IDS[@]}" -eq 0 ]]; then
208
+ SESSION_IDS=("default")
209
+ SEEN_SESSION_IDS["default"]=1
210
+ fi
211
+
212
+ if [[ -z "$SESSION_ID" ]]; then
213
+ base_id="${SESSION_PREFIX}-$(date -u +%Y%m%d%H%M%S)"
214
+ generated_id="$base_id"
215
+ suffix=1
216
+ while [[ -n "${SEEN_SESSION_IDS[$generated_id]+x}" ]]; do
217
+ suffix=$((suffix + 1))
218
+ generated_id="${base_id}-${suffix}"
219
+ done
220
+ SESSION_ID="$generated_id"
221
+ fi
222
+
223
+ if [[ -n "${SEEN_SESSION_IDS[$SESSION_ID]+x}" && "$ALLOW_REUSE" != "1" ]]; then
224
+ fail "session_id \"$SESSION_ID\" ja existe. Use --reuse para permitir reutilizacao."
225
+ fi
226
+
227
+ if [[ -z "${SEEN_SESSION_IDS[$SESSION_ID]+x}" ]]; then
228
+ SESSION_IDS+=("$SESSION_ID")
229
+ SEEN_SESSION_IDS["$SESSION_ID"]=1
230
+ fi
231
+
232
+ current_primary_session_id="$(read_env_value BAILEYS_PRIMARY_SESSION_ID)"
233
+ if ! validate_session_id "$current_primary_session_id"; then
234
+ current_primary_session_id="${SESSION_IDS[0]}"
235
+ fi
236
+ if [[ -z "${SEEN_SESSION_IDS[$current_primary_session_id]+x}" ]]; then
237
+ current_primary_session_id="${SESSION_IDS[0]}"
238
+ fi
239
+
240
+ if [[ "$SET_PRIMARY" == "1" ]]; then
241
+ current_primary_session_id="$SESSION_ID"
242
+ fi
243
+
244
+ declare -A SESSION_WEIGHTS=()
245
+ weights_raw="$(read_env_value BAILEYS_SESSION_WEIGHTS)"
246
+ while IFS= read -r raw_entry; do
247
+ [[ -n "$raw_entry" ]] || continue
248
+ separator='='
249
+ if [[ "$raw_entry" == *":"* && "$raw_entry" != *"="* ]]; then
250
+ separator=':'
251
+ fi
252
+ if [[ "$raw_entry" != *"$separator"* ]]; then
253
+ continue
254
+ fi
255
+
256
+ raw_session="${raw_entry%%"$separator"*}"
257
+ raw_weight="${raw_entry#*"$separator"}"
258
+ if ! validate_session_id "$raw_session"; then
259
+ continue
260
+ fi
261
+ if is_valid_weight "$raw_weight"; then
262
+ SESSION_WEIGHTS["$raw_session"]="$raw_weight"
263
+ fi
264
+ done < <(split_entries "$weights_raw")
265
+
266
+ SESSION_WEIGHTS["$SESSION_ID"]="$SESSION_WEIGHT"
267
+ for existing_session in "${SESSION_IDS[@]}"; do
268
+ if [[ -z "${SESSION_WEIGHTS[$existing_session]+x}" ]]; then
269
+ SESSION_WEIGHTS["$existing_session"]=1
270
+ fi
271
+ done
272
+
273
+ session_ids_value="$(IFS=,; printf '%s' "${SESSION_IDS[*]}")"
274
+ declare -a weight_entries=()
275
+ for listed_session in "${SESSION_IDS[@]}"; do
276
+ weight_entries+=("${listed_session}=${SESSION_WEIGHTS[$listed_session]}")
277
+ done
278
+ weights_value="$(IFS=,; printf '%s' "${weight_entries[*]}")"
279
+
280
+ upsert_env_value "BAILEYS_SESSION_IDS" "$session_ids_value"
281
+ upsert_env_value "BAILEYS_PRIMARY_SESSION_ID" "$current_primary_session_id"
282
+ upsert_env_value "BAILEYS_SESSION_WEIGHTS" "$weights_value"
283
+
284
+ legacy_auth_session_id="$(read_env_value BAILEYS_AUTH_SESSION_ID)"
285
+ if [[ -z "$legacy_auth_session_id" || "$SET_PRIMARY" == "1" ]]; then
286
+ upsert_env_value "BAILEYS_AUTH_SESSION_ID" "$current_primary_session_id"
287
+ fi
288
+
289
+ if [[ "$RESET_AUTH" == "1" ]]; then
290
+ reset_args=(--session "$SESSION_ID")
291
+ if [[ "$CLEAR_AUTH_FILES" == "1" ]]; then
292
+ reset_args+=(--clear-auth-files)
293
+ fi
294
+ bash "$PROJECT_ROOT/scripts/clear-whatsapp-session.sh" "${reset_args[@]}"
295
+ fi
296
+
297
+ log "Sessao preparada com sucesso."
298
+ log "session_id: $SESSION_ID"
299
+ log "BAILEYS_SESSION_IDS: $session_ids_value"
300
+ log "BAILEYS_PRIMARY_SESSION_ID: $current_primary_session_id"
301
+
302
+ if [[ "$CONNECT_NOW" == "1" ]]; then
303
+ require_cmd node
304
+ log "Inicializando schema do banco..."
305
+ node "$PROJECT_ROOT/database/init.js"
306
+
307
+ log "Abrindo conexao da sessao '$SESSION_ID' para leitura do QR Code..."
308
+ export BAILEYS_SESSION_IDS="$SESSION_ID"
309
+ export BAILEYS_PRIMARY_SESSION_ID="$SESSION_ID"
310
+ export BAILEYS_AUTH_SESSION_ID="$SESSION_ID"
311
+
312
+ log "Apos conectar no WhatsApp, use Ctrl+C para encerrar."
313
+ exec node "$PROJECT_ROOT/app/connection/socketController.js"
314
+ fi
315
+
316
+ log "Concluido sem abrir conexao. Para abrir QR agora use:"
317
+ log " BAILEYS_SESSION_IDS=$SESSION_ID BAILEYS_PRIMARY_SESSION_ID=$SESSION_ID BAILEYS_AUTH_SESSION_ID=$SESSION_ID node app/connection/socketController.js"
@@ -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
+ });