Add CI gates, OpenAPI & RLS validation, VPS deploy, logger redaction and idempotency support#132
Add CI gates, OpenAPI & RLS validation, VPS deploy, logger redaction and idempotency support#132adm01-debug wants to merge 9 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
WalkthroughPR adiciona containerização e workflow de deploy VPS; expande CI com gates (typecheck, lint, domain, OpenAPI, perf); publica OpenAPI com validação; centraliza redaction no logger; adiciona idempotência ao public-api; aplica migrações RLS; inclui scripts operacionais para readiness/backup/drift. ChangesContainerização, observabilidade, compliance e validação de build
Sequence DiagramsequenceDiagram
participant Client
participant API as public-api
participant DB
Client->>API: POST /public-api (x-idempotency-key: KEY)
API->>DB: SELECT * FROM messages WHERE request_id = KEY
alt prior message exists
DB-->>API: prior message metadata
API-->>Client: 200 {success:true, duplicate:true}
else new request
API->>DB: INSERT message (request_id = traceId)
DB-->>API: insert ok
API-->>Client: 200 {success:true}
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
Pull request overview
This PR significantly hardens release readiness on multiple fronts: it adds new CI gates (strict typecheck for core modules, OpenAPI validation, VPS readiness checks, performance budget), introduces a containerized VPS deploy flow (Dockerfile, docker-compose, nginx, manual workflow), tightens security/compliance via a static RLS auditor + corresponding migration and PR-time dependency review, adds a redaction layer wired into the frontend logger, and makes the public-api Edge Function safer under retries via an idempotency key.
Changes:
- New CI gates (typecheck:strict:core, domain/barrel checks, api:validate, vps:check, perf:budget) and supporting scripts/configs.
- VPS deploy artifacts (Dockerfile, docker-compose, nginx, healthz/version, deploy-vps workflow).
- Security/compliance: RLS gap-closure migration, RLS verifier script, dependency review job, logger redaction module, idempotency support in
public-api.
Reviewed changes
Copilot reviewed 26 out of 27 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| .github/workflows/ci.yml | Adds typecheck/strict/domain/api/vps/perf gates to the main CI job. |
| .github/workflows/security.yml | Splits dependency-review (PR) and RLS audit (always) jobs. |
| .github/workflows/deploy-vps.yml | New manual workflow building a GHCR image and restarting the VPS via SSH. |
| Dockerfile / docker-compose.yml / nginx.conf / .dockerignore | New container runtime + Traefik-friendly compose for VPS deploy. |
| public/healthz / public/version.json | Static health and version endpoints served by nginx. |
| vite.config.ts | Adds manualChunks, target, sourcemap and chunk-size budget knobs. |
| tsconfig.strict.core.json | Strict tsconfig scoped to redaction.ts and logger.ts. |
| package.json / bun.lock | Adds new scripts and lint-staged dev dependency. |
| scripts/check-performance-budget.mjs | Post-build gzip budget validator for JS/CSS bundles. |
| scripts/check-vps-readiness.mjs | Repository-side checks ensuring VPS artifacts/config exist. |
| scripts/validate-openapi.mjs | Lightweight OpenAPI spec & Edge-Function mapping validator. |
| scripts/verify_rls_compliance.ts | Static SQL parser ensuring every table has RLS + policy. |
| docs/api/openapi.json | Initial OpenAPI 3.1 contract for key Edge Functions. |
| docs/security/auth-baseline.md / docs/observability/slos.md / docs/auditorias/...md / CHANGELOG.md | Operational documentation additions. |
| src/lib/redaction.ts / src/lib/redaction.test.ts | New PII/secret redaction helpers + tests. |
| src/lib/logger.ts | Wires redactLogArgs / stringifyRedacted into all log paths and Sentry extras. |
| supabase/functions/public-api/index.ts | Adds idempotency-key parsing, replay short-circuit, and trace stamping. |
| supabase/migrations/20260513093000_rls_compliance_gap_closure.sql | Enables RLS and adds policies for previously-uncovered tables. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (previousMessage) { | ||
| log.info('Idempotency replay', { idempotencyKey, messageId: previousMessage.id }); | ||
| return jsonResponse( | ||
| { | ||
| success: true, | ||
| duplicate: true, | ||
| messageId: previousMessage.id, | ||
| contactId: previousMessage.contact_id, | ||
| status: previousMessage.status, | ||
| externalId: previousMessage.external_id, | ||
| requestId, | ||
| }, | ||
| 200, | ||
| req | ||
| ); | ||
| } |
| if (idempotencyKey) { | ||
| const { data: previousMessage } = await supabase | ||
| .from('messages') | ||
| .select('id, contact_id, status, external_id') | ||
| .eq('request_id', idempotencyKey) | ||
| .maybeSingle(); | ||
|
|
||
| if (previousMessage) { | ||
| log.info('Idempotency replay', { idempotencyKey, messageId: previousMessage.id }); | ||
| return jsonResponse( | ||
| { | ||
| success: true, | ||
| duplicate: true, | ||
| messageId: previousMessage.id, | ||
| contactId: previousMessage.contact_id, | ||
| status: previousMessage.status, | ||
| externalId: previousMessage.external_id, | ||
| requestId, | ||
| }, | ||
| 200, | ||
| req | ||
| ); | ||
| } | ||
| } |
| const JWT_PATTERN = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g; | ||
| const UUID_TOKEN_PATTERN = | ||
| /\b(?:[A-F0-9]{8}-[A-F0-9]{4}-[1-5][A-F0-9]{3}-[89AB][A-F0-9]{3}-[A-F0-9]{12})\b/gi; | ||
| const BRAZIL_PHONE_PATTERN = /(?<!\d)(?:\+?55\s?)?(?:\(?\d{2}\)?\s?)?9?\d{4}[-\s]?\d{4}(?!\d)/g; |
| -- 3) Avatars: leitura autenticada; usuários gerenciam seus próprios metadados; admins gerenciam tudo. | ||
| DROP POLICY IF EXISTS rls_avatars_select_authenticated ON public.avatars; | ||
| CREATE POLICY rls_avatars_select_authenticated ON public.avatars FOR SELECT TO authenticated USING (true); | ||
| DROP POLICY IF EXISTS rls_avatars_manage_own ON public.avatars; | ||
| CREATE POLICY rls_avatars_manage_own ON public.avatars FOR ALL TO authenticated USING (user_id = auth.uid()) WITH CHECK (user_id = auth.uid()); | ||
| DROP POLICY IF EXISTS rls_avatars_admin_manage ON public.avatars; | ||
| CREATE POLICY rls_avatars_admin_manage ON public.avatars FOR ALL TO authenticated | ||
| USING (public.has_role(auth.uid(), 'admin'::public.app_role)) | ||
| WITH CHECK (public.has_role(auth.uid(), 'admin'::public.app_role)); | ||
|
|
||
| -- 4) Templates de e-mail: leitura para autenticados; criador/admin gerenciam. | ||
| DROP POLICY IF EXISTS rls_email_templates_select_authenticated ON public.email_templates; | ||
| CREATE POLICY rls_email_templates_select_authenticated ON public.email_templates FOR SELECT TO authenticated USING (true); | ||
| DROP POLICY IF EXISTS rls_email_templates_manage_creator_or_admin ON public.email_templates; | ||
| CREATE POLICY rls_email_templates_manage_creator_or_admin ON public.email_templates FOR ALL TO authenticated | ||
| USING (created_by = auth.uid() OR public.has_role(auth.uid(), 'admin'::public.app_role)) | ||
| WITH CHECK (created_by = auth.uid() OR public.has_role(auth.uid(), 'admin'::public.app_role)); |
| -- 5) Contatos multi-telefone: leitura autenticada; escrita admin/supervisor. | ||
| DROP POLICY IF EXISTS rls_contact_phones_select_authenticated ON public.contact_phones; | ||
| CREATE POLICY rls_contact_phones_select_authenticated ON public.contact_phones FOR SELECT TO authenticated USING (true); | ||
| DROP POLICY IF EXISTS rls_contact_phones_admin_manage ON public.contact_phones; | ||
| CREATE POLICY rls_contact_phones_admin_manage ON public.contact_phones FOR ALL TO authenticated | ||
| USING (public.has_role(auth.uid(), 'admin'::public.app_role) OR public.has_role(auth.uid(), 'supervisor'::public.app_role)) | ||
| WITH CHECK (public.has_role(auth.uid(), 'admin'::public.app_role) OR public.has_role(auth.uid(), 'supervisor'::public.app_role)); | ||
|
|
||
| -- 6) IMAP/SMTP: usuário gerencia suas próprias credenciais; admins podem auditar/gerenciar. | ||
| DROP POLICY IF EXISTS rls_imap_smtp_accounts_manage_own ON public.imap_smtp_accounts; | ||
| CREATE POLICY rls_imap_smtp_accounts_manage_own ON public.imap_smtp_accounts FOR ALL TO authenticated USING (user_id = auth.uid()) WITH CHECK (user_id = auth.uid()); | ||
| DROP POLICY IF EXISTS rls_imap_smtp_accounts_admin_manage ON public.imap_smtp_accounts; | ||
| CREATE POLICY rls_imap_smtp_accounts_admin_manage ON public.imap_smtp_accounts FOR ALL TO authenticated | ||
| USING (public.has_role(auth.uid(), 'admin'::public.app_role)) | ||
| WITH CHECK (public.has_role(auth.uid(), 'admin'::public.app_role)); | ||
|
|
||
| -- 7) Salespeople: leitura autenticada para roteamento/CRM; escrita restrita a admin/supervisor. | ||
| DROP POLICY IF EXISTS rls_salespeople_select_authenticated ON public.salespeople; | ||
| CREATE POLICY rls_salespeople_select_authenticated ON public.salespeople FOR SELECT TO authenticated USING (true); | ||
| DROP POLICY IF EXISTS rls_salespeople_admin_supervisor_manage ON public.salespeople; | ||
| CREATE POLICY rls_salespeople_admin_supervisor_manage ON public.salespeople FOR ALL TO authenticated | ||
| USING (public.has_role(auth.uid(), 'admin'::public.app_role) OR public.has_role(auth.uid(), 'supervisor'::public.app_role)) | ||
| WITH CHECK (public.has_role(auth.uid(), 'admin'::public.app_role) OR public.has_role(auth.uid(), 'supervisor'::public.app_role)); |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1dc0c9b14c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| -- 1) Habilita RLS nas tabelas que ainda não tinham enforcement explícito. | ||
| ALTER TABLE IF EXISTS public.avatars ENABLE ROW LEVEL SECURITY; | ||
| ALTER TABLE IF EXISTS public.channel_connections_safe ENABLE ROW LEVEL SECURITY; |
There was a problem hiding this comment.
Avoid enabling RLS on the safe view
In migrated databases public.channel_connections_safe is a view: supabase/migrations/20260404173531_a535b696-2e46-42d5-84bb-9ee0ed43fa83.sql creates it with CREATE OR REPLACE VIEW, and the later CREATE TABLE IF NOT EXISTS does not replace that relation. This new migration will therefore try to apply table RLS to a view (and the subsequent CREATE POLICY ... ON public.channel_connections_safe would do the same), causing the migration to fail before any of the gap-closure policies are applied. The policy should target the underlying table or the view grants/security-invoker behavior instead.
Useful? React with 👍 / 👎.
| if (idempotencyKey) { | ||
| const { data: previousMessage } = await supabase | ||
| .from('messages') | ||
| .select('id, contact_id, status, external_id') | ||
| .eq('request_id', idempotencyKey) | ||
| .maybeSingle(); |
There was a problem hiding this comment.
Make idempotency checks atomic
When two retries with the same x-idempotency-key arrive concurrently, both can execute this lookup before either insert commits, then both insert and send a WhatsApp message. The repo only has a normal idx_messages_request_id index for messages.request_id (supabase/migrations/20260423104838_663a01ab-2832-4f21-96c1-961a81155177.sql), so nothing prevents duplicate rows for the same key. Idempotency needs a unique constraint plus insert/upsert conflict handling (or another atomic reservation) before the external send.
Useful? React with 👍 / 👎.
|
|
||
| -- 5) Contatos multi-telefone: leitura autenticada; escrita admin/supervisor. | ||
| DROP POLICY IF EXISTS rls_contact_phones_select_authenticated ON public.contact_phones; | ||
| CREATE POLICY rls_contact_phones_select_authenticated ON public.contact_phones FOR SELECT TO authenticated USING (true); |
There was a problem hiding this comment.
Restrict contact phone reads to authorized users
This policy gives every authenticated role full-table SELECT access to contact_phones, whose table definition stores customer phone_number values. Because RLS was already enabled for this table and there were no existing policies for it, this changes the default from no access to all authenticated users seeing all contact phone numbers, including agents/viewers outside the relevant contact scope. Please scope reads through the related contact ownership/assignment or privileged roles instead of USING (true).
Useful? React with 👍 / 👎.
| COPY . . | ||
|
|
||
| ARG VITE_SUPABASE_URL | ||
| ARG VITE_SUPABASE_ANON_KEY |
There was a problem hiding this comment.
Provide the publishable Supabase key in VPS builds
The new VPS image only injects VITE_SUPABASE_ANON_KEY, but this app still has many runtime clients that read import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY directly for Edge Function apikey/Bearer headers (for example voice, TTS, proxy, and web-vitals paths). In Docker/VPS deployments those references are compiled as undefined, so those features call Supabase functions with invalid credentials even though the anon key was provided. Add a VITE_SUPABASE_PUBLISHABLE_KEY build arg/env or map it from the anon key in the Docker/compose/workflow path.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 11
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/deploy-vps.yml:
- Line 99: O comando "docker compose pull zapp-web || true" está mascarando
falhas no pull; remova o "|| true" para permitir que a etapa falhe
explicitamente, ou substitua por lógica de erro que capture e registre a falha
antes de abortar (por exemplo, executar o pull, checar o exit code e usar um
echo/processo de logging + exit 1) — localize a ocorrência da string "docker
compose pull zapp-web || true" e altere-a para uma das duas abordagens: (1)
permitir o erro natural removendo "|| true", ou (2) adicionar checagem/registro
do erro e abortar o job quando o pull falhar.
- Line 101: O comando "docker image prune -f" é destrutivo e remove todas as
imagens dangling no VPS; substitua-o por uma ação segura: ao invés de usar
"docker image prune -f" no workflow, filtre apenas imagens do
repositório/serviço (por exemplo usando --filter com label ou reference) ou
remova explicitamente a imagem anterior do zapp-web somente após confirmar que a
nova está em execução (por exemplo, obter o ID/TAG antigo e executar docker
image rm <OLD_IMAGE> depois da verificação). Localize e substituir a ocorrência
"docker image prune -f" no arquivo .github/workflows/deploy-vps.yml (referência:
o token "docker image prune -f") por uma dessas alternativas seguras.
In `@Dockerfile`:
- Around line 30-36: Substitua a imagem base em Dockerfile de nginx:1.27-alpine
para nginxinc/nginx-unprivileged:1.27-alpine, atualize EXPOSE para 8080 e ajuste
o HEALTHCHECK para usar http://127.0.0.1:8080/healthz; adicione também a
alteração correspondente em nginx.conf (mudar listen para 8080) e em
docker-compose.yml atualize a label
traefik.http.services.zapp-web.loadbalancer.server.port para 8080 para que todo
o stack use a porta não-privilegiada da imagem unprivileged.
In `@docs/api/openapi.json`:
- Around line 155-160: Resumo: o OpenAPI para a rota "/public-api" declara 401 e
429 mas não documenta 403, embora a implementação retorne 403 para chave API
presente porém inválida; adicione a resposta 403 para manter o contrato
alinhado. Instruções: edite o objeto de respostas da operação "/public-api" no
openapi.json e inclua a entrada "403" referenciando um componente de resposta
apropriado (por exemplo "$ref": "#/components/responses/Forbidden"); se não
existir, crie components.responses.Forbidden com a mensagem/esquema esperado
(mesmo formato usado por Unauthorized/RateLimited) e atualize qualquer exemplo
ou descrição para indicar “x-api-key presente porém inválida”. Certifique-se de
manter a consistência com as outras response refs (Unauthorized/RateLimited)
para evitar que a especificação fique fora do contrato.
In `@scripts/check-performance-budget.mjs`:
- Around line 10-15: The current budgets object (initialJsGzipKb, chunkJsGzipKb,
totalJsGzipKb, totalCssGzipKb) uses Number(...) on env vars which yields NaN for
invalid input and silently bypasses the gate; update the parsing to validate
each parsed value (e.g. via Number(value) then check Number.isFinite(...) and a
sensible >=0 threshold) and either replace invalid values with the intended
defaults or throw/exit with an error so the check fails loudly; implement this
validation immediately after computing each budget entry (or in a small helper
used by those four keys) so CI/build cannot pass with malformed PERF_BUDGET_*
env values.
In `@scripts/check-vps-readiness.mjs`:
- Around line 46-47: The current checks in the add(...) calls are unsafe: the
first regex only blocks one specific cloud project_id and the second inspects
supabaseConfig raw text so an endpoint in a comment can bypass validation;
update the project_id check to detect any project_id assignment (e.g., match
/project_id\s*=\s*["'][^"']+["']/) against activeSupabaseConfig and ensure you
validate the sanitized config for the endpoint by stripping comments from
supabaseConfig (remove line comments and block comments) before testing for
/supabase\.atomicabr\.com\.br/; modify the two add(...) validations that
reference activeSupabaseConfig and supabaseConfig accordingly so they operate on
sanitized input and a generic project_id pattern.
In `@scripts/verify_rls_compliance.ts`:
- Line 1: Não assuma que diretórios raiz existem: envolva chamadas a
readdirSync(dir) (e o bloco que itera sobre os roots nas linhas correspondentes,
p.ex. uso em 14-18) com uma verificação/tratamento de erro; antes de chamar
readdirSync use statSync/existsSync para pular diretórios ausentes ou capture
ENOENT em um try/catch e simplesmente continuar (logando debug) para que a
verificação de RLS não falhe quando supabase/migrations-from-lovable não estiver
presente.
In `@src/lib/redaction.ts`:
- Around line 21-23: The current redaction chain runs BRAZIL_PHONE_PATTERN
before WHATSAPP_JID_PATTERN causing phone-only replacement to leave the JID
domain (e.g., "@s.whatsapp.net") visible; modify the replace order so
WHATSAPP_JID_PATTERN is applied before BRAZIL_PHONE_PATTERN (or change
WHATSAPP_JID_PATTERN replacement to return the full REDACTED token instead of
`${REDACTED}@$1`) in the pipeline that uses .replace(BRAZIL_PHONE_PATTERN,...).
Ensure the call sequence contains .replace(WHATSAPP_JID_PATTERN, REDACTED) (or
moves that replace earlier) followed by .replace(BRAZIL_PHONE_PATTERN, REDACTED)
and keep UUID_TOKEN_PATTERN replacement last.
In `@supabase/functions/public-api/index.ts`:
- Around line 9-13: The getIdempotencyKey function currently treats an
out-of-range x-idempotency-key as null and silently continues; change the logic
so that if req.headers contains 'x-idempotency-key' or 'idempotency-key' but the
trimmed value length is not between 8 and 200 the request is rejected with a 400
response. Update getIdempotencyKey (or add a companion validator) to distinguish
three outcomes (missing, valid key, invalid key) and modify the request handler
that calls getIdempotencyKey to return a 400 when the outcome is "invalid" (use
the function name getIdempotencyKey and the top-level request handler that
invokes it to locate the code). Ensure the 400 response includes a clear error
message about the invalid idempotency key.
In `@supabase/migrations/20260513093000_rls_compliance_gap_closure.sql`:
- Around line 94-99: A política rls_contact_phones_select_authenticated está
permitindo SELECT para qualquer usuário autenticado porque usa USING (true);
substitua esse predicado por uma condição que restrinja visibilidade por
tenant/owner/role em vez de global — por exemplo, trocar o USING (true) da
policy rls_contact_phones_select_authenticated por uma expressão que compare
contact_phones.tenant_id ou contact_phones.owner_id com o valor do token de
sessão (ou invoque uma função utilitária existente como public.current_tenant()
ou public.has_role(...)) para garantir isolamento horizontal; mantenha ou ajuste
a policy rls_contact_phones_admin_manage conforme necessário para
administradores/supervisores usando public.has_role(auth.uid(), ...) como já
feito.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 4743374e-55cf-49c8-b40a-4787511e49cf
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock,!**/*.lock
📒 Files selected for processing (26)
.dockerignore.github/workflows/ci.yml.github/workflows/deploy-vps.yml.github/workflows/security.ymlCHANGELOG.mdDockerfiledocker-compose.ymldocs/api/openapi.jsondocs/auditorias/2026-05-13-auditoria-tecnica-exaustiva.mddocs/observability/slos.mddocs/security/auth-baseline.mdnginx.confpackage.jsonpublic/healthzpublic/version.jsonscripts/check-performance-budget.mjsscripts/check-vps-readiness.mjsscripts/validate-openapi.mjsscripts/verify_rls_compliance.tssrc/lib/logger.tssrc/lib/redaction.test.tssrc/lib/redaction.tssupabase/functions/public-api/index.tssupabase/migrations/20260513093000_rls_compliance_gap_closure.sqltsconfig.strict.core.jsonvite.config.ts
| export ZAPP_WEB_HOST="${ZAPP_WEB_HOST:-zapp.atomicabr.com.br}" | ||
| docker compose pull zapp-web || true | ||
| docker compose up -d zapp-web | ||
| docker image prune -f |
There was a problem hiding this comment.
docker image prune -f é operação destrutiva não escopada.
Este comando remove todas as imagens dangling do VPS, não apenas as relacionadas ao zapp-web. Se houver outros serviços no mesmo servidor, imagens deles podem ser removidas inadvertidamente.
🔧 Alternativa mais segura
Filtrar apenas imagens do repositório específico:
- docker image prune -f
+ docker image prune -f --filter "label=org.opencontainers.image.source=https://github.com/${{ github.repository }}"Ou remover apenas a imagem anterior após confirmar que a nova está rodando:
- docker image prune -f
+ # Prune apenas se deploy bem-sucedido
+ docker compose ps zapp-web --format '{{.Status}}' | grep -q 'Up' && docker image prune -f --filter "until=24h"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/deploy-vps.yml at line 101, O comando "docker image prune
-f" é destrutivo e remove todas as imagens dangling no VPS; substitua-o por uma
ação segura: ao invés de usar "docker image prune -f" no workflow, filtre apenas
imagens do repositório/serviço (por exemplo usando --filter com label ou
reference) ou remova explicitamente a imagem anterior do zapp-web somente após
confirmar que a nova está em execução (por exemplo, obter o ID/TAG antigo e
executar docker image rm <OLD_IMAGE> depois da verificação). Localize e
substituir a ocorrência "docker image prune -f" no arquivo
.github/workflows/deploy-vps.yml (referência: o token "docker image prune -f")
por uma dessas alternativas seguras.
| FROM nginx:1.27-alpine AS runtime | ||
| COPY nginx.conf /etc/nginx/conf.d/default.conf | ||
| COPY --from=builder /app/dist /usr/share/nginx/html | ||
| EXPOSE 80 | ||
| HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ | ||
| CMD wget -q -O - http://127.0.0.1/healthz >/dev/null || exit 1 | ||
| CMD ["nginx", "-g", "daemon off;"] |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Dockerfile: instruções relevantes =="
rg -n '^(FROM|USER|EXPOSE|HEALTHCHECK|CMD)\b' Dockerfile
echo
echo "== Verificação de USER explícito =="
if rg -n '^USER\s+' Dockerfile; then
echo "OK: há USER explícito."
else
echo "FALHA: não há USER explícito."
fiRepository: adm01-debug/zapp-web
Length of output: 393
🏁 Script executed:
# Procurar pelo nginx.conf
fd -name "nginx.conf" -type fRepository: adm01-debug/zapp-web
Length of output: 295
🏁 Script executed:
# Procurar pelo docker-compose.yml
fd -name "docker-compose.yml" -o -name "docker-compose.yaml" -type fRepository: adm01-debug/zapp-web
Length of output: 295
🌐 Web query:
nginxinc/nginx-unprivileged:1.27-alpine docker image availability
💡 Result:
The Docker image nginxinc/nginx-unprivileged:1.27-alpine is available on Docker Hub. [1][2] Evidence: - Docker Hub shows the existence of image layer details specifically for the tag nginxinc/nginx-unprivileged:1.27-alpine (two layer image digests are listed under that tag page), indicating the tag has been published and has stored image layers retrievable from Docker Hub. [1][2] - The nginx/docker-nginx-unprivileged project publishes pre-built images to registries including Docker Hub and describes that “the last built tag of every release is kept indefinitely” in those registries (so a released tag like 1.27-alpine should remain available barring extraordinary registry-side changes). [3] Practical check / pull: - You can verify directly with: docker pull nginxinc/nginx-unprivileged:1.27-alpine (the tag is expected to work given the Docker Hub tag’s layer availability). [1][2] Notes on tag naming (to avoid confusion): - This repository has many tag variants (e.g., -perl or with Alpine/OS versions). The exact tag requested is 1.27-alpine, not alpine3.x-specific tags. [4]
Citations:
- 1: https://hub.docker.com/layers/nginxinc/nginx-unprivileged/1.27-alpine/images/sha256-9e3df2a002908b436d439ea61432e6d2c19985343db08ad64541eb53dd204fb2
- 2: https://hub.docker.com/layers/nginxinc/nginx-unprivileged/1.27-alpine/images/sha256-cf3dbbb8be91731ff2eeedb71c82347b65b32636b6fbe88f101e0c6b0e20fd82
- 3: https://github.com/nginxinc/docker-nginx-unprivileged
- 4: https://hub.docker.com/r/nginxinc/nginx-unprivileged/tags
🏁 Script executed:
fd nginx.confRepository: adm01-debug/zapp-web
Length of output: 127
🏁 Script executed:
fd docker-composeRepository: adm01-debug/zapp-web
Length of output: 83
🏁 Script executed:
cat -n nginx.confRepository: adm01-debug/zapp-web
Length of output: 1528
🏁 Script executed:
cat -n docker-compose.ymlRepository: adm01-debug/zapp-web
Length of output: 1593
Remova acesso root da imagem nginx no runtime.
O container roda como root porque nginx:1.27-alpine não define usuário não-privilegiado. Em caso de comprometimento, o atacante tem acesso irrestrito. Use nginxinc/nginx-unprivileged:1.27-alpine + ajuste a porta para 8080 em todos os locais:
nginx.conf:listen 8080;(linha 2)Dockerfile:EXPOSE 8080(linha 33)Dockerfile HEALTHCHECK:http://127.0.0.1:8080/healthz(linha 35)docker-compose.yml:traefik.http.services.zapp-web.loadbalancer.server.port=8080(linha 24)
A imagem unprivileged roda na porta 8080 por padrão e não pode ouvir em portas < 1024 sem privilégios.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Dockerfile` around lines 30 - 36, Substitua a imagem base em Dockerfile de
nginx:1.27-alpine para nginxinc/nginx-unprivileged:1.27-alpine, atualize EXPOSE
para 8080 e ajuste o HEALTHCHECK para usar http://127.0.0.1:8080/healthz;
adicione também a alteração correspondente em nginx.conf (mudar listen para
8080) e em docker-compose.yml atualize a label
traefik.http.services.zapp-web.loadbalancer.server.port para 8080 para que todo
o stack use a porta não-privilegiada da imagem unprivileged.
| "401": { | ||
| "$ref": "#/components/responses/Unauthorized" | ||
| }, | ||
| "429": { | ||
| "$ref": "#/components/responses/RateLimited" | ||
| } |
There was a problem hiding this comment.
Documente o 403 do /public-api.
A implementação retorna 403 quando o x-api-key existe mas é inválido, mas o contrato só declara 401. Isso deixa a resposta fora do spec para um caso normal de autenticação.
🔧 Ajuste sugerido
"responses": {
"200": {
"description": "Comando aceito/processado.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CommandResponse"
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
"429": {
"$ref": "#/components/responses/RateLimited"
}
},📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "401": { | |
| "$ref": "#/components/responses/Unauthorized" | |
| }, | |
| "429": { | |
| "$ref": "#/components/responses/RateLimited" | |
| } | |
| "401": { | |
| "$ref": "#/components/responses/Unauthorized" | |
| }, | |
| "403": { | |
| "$ref": "#/components/responses/Forbidden" | |
| }, | |
| "429": { | |
| "$ref": "#/components/responses/RateLimited" | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/api/openapi.json` around lines 155 - 160, Resumo: o OpenAPI para a rota
"/public-api" declara 401 e 429 mas não documenta 403, embora a implementação
retorne 403 para chave API presente porém inválida; adicione a resposta 403 para
manter o contrato alinhado. Instruções: edite o objeto de respostas da operação
"/public-api" no openapi.json e inclua a entrada "403" referenciando um
componente de resposta apropriado (por exemplo "$ref":
"#/components/responses/Forbidden"); se não existir, crie
components.responses.Forbidden com a mensagem/esquema esperado (mesmo formato
usado por Unauthorized/RateLimited) e atualize qualquer exemplo ou descrição
para indicar “x-api-key presente porém inválida”. Certifique-se de manter a
consistência com as outras response refs (Unauthorized/RateLimited) para evitar
que a especificação fique fora do contrato.
| const budgets = { | ||
| initialJsGzipKb: Number(process.env.PERF_BUDGET_INITIAL_JS_GZIP_KB ?? 1500), | ||
| chunkJsGzipKb: Number(process.env.PERF_BUDGET_CHUNK_JS_GZIP_KB ?? 1500), | ||
| totalJsGzipKb: Number(process.env.PERF_BUDGET_TOTAL_JS_GZIP_KB ?? 2700), | ||
| totalCssGzipKb: Number(process.env.PERF_BUDGET_TOTAL_CSS_GZIP_KB ?? 250), | ||
| }; |
There was a problem hiding this comment.
Valide budgets de ambiente para evitar bypass silencioso do gate
Hoje, se uma env vier inválida, Number(...) vira NaN e as comparações não disparam falha. Resultado: o check pode “passar” mesmo sem limite válido.
💡 Patch sugerido
+function readBudget(name, fallback) {
+ const raw = process.env[name];
+ if (raw == null || raw === '') return fallback;
+ const parsed = Number(raw);
+ if (!Number.isFinite(parsed) || parsed <= 0) {
+ console.error(`❌ Valor inválido para ${name}: "${raw}"`);
+ process.exit(1);
+ }
+ return parsed;
+}
+
const budgets = {
- initialJsGzipKb: Number(process.env.PERF_BUDGET_INITIAL_JS_GZIP_KB ?? 1500),
- chunkJsGzipKb: Number(process.env.PERF_BUDGET_CHUNK_JS_GZIP_KB ?? 1500),
- totalJsGzipKb: Number(process.env.PERF_BUDGET_TOTAL_JS_GZIP_KB ?? 2700),
- totalCssGzipKb: Number(process.env.PERF_BUDGET_TOTAL_CSS_GZIP_KB ?? 250),
+ initialJsGzipKb: readBudget('PERF_BUDGET_INITIAL_JS_GZIP_KB', 1500),
+ chunkJsGzipKb: readBudget('PERF_BUDGET_CHUNK_JS_GZIP_KB', 1500),
+ totalJsGzipKb: readBudget('PERF_BUDGET_TOTAL_JS_GZIP_KB', 2700),
+ totalCssGzipKb: readBudget('PERF_BUDGET_TOTAL_CSS_GZIP_KB', 250),
};🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/check-performance-budget.mjs` around lines 10 - 15, The current
budgets object (initialJsGzipKb, chunkJsGzipKb, totalJsGzipKb, totalCssGzipKb)
uses Number(...) on env vars which yields NaN for invalid input and silently
bypasses the gate; update the parsing to validate each parsed value (e.g. via
Number(value) then check Number.isFinite(...) and a sensible >=0 threshold) and
either replace invalid values with the intended defaults or throw/exit with an
error so the check fails loudly; implement this validation immediately after
computing each budget entry (or in a small helper used by those four keys) so
CI/build cannot pass with malformed PERF_BUDGET_* env values.
| @@ -0,0 +1,98 @@ | |||
| import { readdirSync, readFileSync, statSync } from 'fs'; | |||
There was a problem hiding this comment.
Não assuma que todos os roots existem.
readdirSync(dir) lança ENOENT se supabase/migrations-from-lovable não estiver presente no checkout. Nesse caso o gate falha antes de auditar qualquer migration.
🔧 Ajuste sugerido
-import { readdirSync, readFileSync, statSync } from 'fs';
+import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
@@
function walk(dir: string, files: string[] = []): string[] {
+ if (!existsSync(dir)) return files;
for (const entry of readdirSync(dir)) {
const path = join(dir, entry);
if (statSync(path).isDirectory()) walk(path, files);
else if (path.endsWith('.sql')) files.push(path);
}Also applies to: 14-18
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/verify_rls_compliance.ts` at line 1, Não assuma que diretórios raiz
existem: envolva chamadas a readdirSync(dir) (e o bloco que itera sobre os roots
nas linhas correspondentes, p.ex. uso em 14-18) com uma verificação/tratamento
de erro; antes de chamar readdirSync use statSync/existsSync para pular
diretórios ausentes ou capture ENOENT em um try/catch e simplesmente continuar
(logando debug) para que a verificação de RLS não falhe quando
supabase/migrations-from-lovable não estiver presente.
| .replace(BRAZIL_PHONE_PATTERN, REDACTED) | ||
| .replace(WHATSAPP_JID_PATTERN, `${REDACTED}@$1`) | ||
| .replace(UUID_TOKEN_PATTERN, REDACTED); |
There was a problem hiding this comment.
Ordem de redaction pode vazar parte do JID do WhatsApp
Do jeito atual, o pattern de telefone pode rodar antes do JID e manter @s.whatsapp.net no log. Melhor redigir JID antes (ou substituir JID inteiro por [REDACTED]).
💡 Patch sugerido
function redactString(value: string): string {
return value
.replace(BEARER_PATTERN, `Bearer ${REDACTED}`)
.replace(JWT_PATTERN, REDACTED)
.replace(EMAIL_PATTERN, REDACTED)
+ .replace(WHATSAPP_JID_PATTERN, REDACTED)
.replace(CPF_CNPJ_PATTERN, REDACTED)
.replace(BRAZIL_PHONE_PATTERN, REDACTED)
- .replace(WHATSAPP_JID_PATTERN, `${REDACTED}@$1`)
.replace(UUID_TOKEN_PATTERN, REDACTED);
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lib/redaction.ts` around lines 21 - 23, The current redaction chain runs
BRAZIL_PHONE_PATTERN before WHATSAPP_JID_PATTERN causing phone-only replacement
to leave the JID domain (e.g., "@s.whatsapp.net") visible; modify the replace
order so WHATSAPP_JID_PATTERN is applied before BRAZIL_PHONE_PATTERN (or change
WHATSAPP_JID_PATTERN replacement to return the full REDACTED token instead of
`${REDACTED}@$1`) in the pipeline that uses .replace(BRAZIL_PHONE_PATTERN,...).
Ensure the call sequence contains .replace(WHATSAPP_JID_PATTERN, REDACTED) (or
moves that replace earlier) followed by .replace(BRAZIL_PHONE_PATTERN, REDACTED)
and keep UUID_TOKEN_PATTERN replacement last.
| function getIdempotencyKey(req: Request): string | null { | ||
| const raw = req.headers.get('x-idempotency-key') ?? req.headers.get('idempotency-key'); | ||
| const key = raw?.trim(); | ||
| if (!key) return null; | ||
| return key.length >= 8 && key.length <= 200 ? key : null; |
There was a problem hiding this comment.
Não silencie x-idempotency-key inválido.
Se o header vier fora do range 8..200, ele vira null e a request segue sem idempotência. Isso mascara erro do cliente e pode gerar reenvio duplicado mesmo quando o chamador acha que está protegido. Retorne 400 quando o header vier presente, mas inválido.
As per coding guidelines, supabase/functions/**/*.ts: "Idempotência em endpoints que recebem retries (Bitrix24, Evolution API, etc)".
Also applies to: 22-23
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@supabase/functions/public-api/index.ts` around lines 9 - 13, The
getIdempotencyKey function currently treats an out-of-range x-idempotency-key as
null and silently continues; change the logic so that if req.headers contains
'x-idempotency-key' or 'idempotency-key' but the trimmed value length is not
between 8 and 200 the request is rejected with a 400 response. Update
getIdempotencyKey (or add a companion validator) to distinguish three outcomes
(missing, valid key, invalid key) and modify the request handler that calls
getIdempotencyKey to return a 400 when the outcome is "invalid" (use the
function name getIdempotencyKey and the top-level request handler that invokes
it to locate the code). Ensure the 400 response includes a clear error message
about the invalid idempotency key.
| DROP POLICY IF EXISTS rls_contact_phones_select_authenticated ON public.contact_phones; | ||
| CREATE POLICY rls_contact_phones_select_authenticated ON public.contact_phones FOR SELECT TO authenticated USING (true); | ||
| DROP POLICY IF EXISTS rls_contact_phones_admin_manage ON public.contact_phones; | ||
| CREATE POLICY rls_contact_phones_admin_manage ON public.contact_phones FOR ALL TO authenticated | ||
| USING (public.has_role(auth.uid(), 'admin'::public.app_role) OR public.has_role(auth.uid(), 'supervisor'::public.app_role)) | ||
| WITH CHECK (public.has_role(auth.uid(), 'admin'::public.app_role) OR public.has_role(auth.uid(), 'supervisor'::public.app_role)); |
There was a problem hiding this comment.
contact_phones ficou legível por qualquer autenticado.
USING (true) no SELECT expõe todos os telefones para qualquer sessão autenticada. Para uma tabela com PII, isso vira vazamento horizontal de dados; a policy precisa ser escopada por tenant, owner ou role de negócio, não aberta globalmente.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@supabase/migrations/20260513093000_rls_compliance_gap_closure.sql` around
lines 94 - 99, A política rls_contact_phones_select_authenticated está
permitindo SELECT para qualquer usuário autenticado porque usa USING (true);
substitua esse predicado por uma condição que restrinja visibilidade por
tenant/owner/role em vez de global — por exemplo, trocar o USING (true) da
policy rls_contact_phones_select_authenticated por uma expressão que compare
contact_phones.tenant_id ou contact_phones.owner_id com o valor do token de
sessão (ou invoque uma função utilitária existente como public.current_tenant()
ou public.has_role(...)) para garantir isolamento horizontal; mantenha ou ajuste
a policy rls_contact_phones_admin_manage conforme necessário para
administradores/supervisores usando public.has_role(auth.uid(), ...) como já
feito.
|
Atualizei o PR com o commit
Validações locais executadas: |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/deploy-vps.yml:
- Around line 91-95: Add SSH host key pinning to the appleboy/ssh-action
invocation by setting the fingerprint parameter to the server's public key
fingerprint (stored as a secret); update the deploy workflow entry that uses
appleboy/ssh-action@v1.2.0 (the block with host, username, key, envs, script) to
include fingerprint: ${{ secrets.VPS_SSH_FINGERPRINT }} so the action validates
the server key before connecting (generate and save the fingerprint using
ssh-keygen as described and add it to repository secrets).
- Around line 19-21: A permissão global "packages: write" é demasiada — remova
ou altere a entrada "packages: write" do bloco permissions e mantenha apenas
"contents: read" no topo; então adicionar explicitamente "permissions: packages:
write" apenas no bloco do job "build-and-push" (onde as ações publicam imagens)
e garanta que o job "deploy" permaneça com leitura (ou sem escrever pacotes), já
que este só executa "docker compose pull" e comandos remotos; atualize a
definição dos jobs referenciando o nome "build-and-push" e verifique que não
exista outro job que precise de write global.
- Around line 100-101: The deploy job currently runs "docker compose up -d
zapp-web" and exits before the service is healthy; modify the workflow to wait
for the container to reach a healthy state after "docker compose up -d zapp-web"
by polling the service health (e.g., use "docker compose ps" or "docker inspect"
on the "zapp-web" container ID or run repeated HTTP probes against the app) and
only exit successfully once the health status is "healthy" or the HTTP probe
returns a 2xx/3xx; fail the step if a configurable timeout is exceeded. Ensure
the wait logic is implemented immediately after the existing "docker compose up
-d zapp-web" command so the job reflects the actual service readiness.
In `@supabase/migrations/20260513113000_pr132_review_hardening.sql`:
- Around line 8-10: The CREATE UNIQUE INDEX idx_messages_request_id_unique on
public.messages(request_id) will fail if legacy duplicate request_id values
exist; before creating the unique index add a pre-check that queries
public.messages grouped by request_id to detect duplicates and raise a clear
error (or abort the migration) if any duplicate count > 1, so the migration
fails early with an explicit message guiding remediation (or alternatively
deduplicate/update those rows prior to creating idx_messages_request_id_unique).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 2fdac33f-ea02-4849-a0c7-a65e5e978eef
📒 Files selected for processing (5)
.github/workflows/deploy-vps.ymlsrc/lib/redaction.test.tssrc/lib/redaction.tssupabase/functions/public-api/index.tssupabase/migrations/20260513113000_pr132_review_hardening.sql
🚧 Files skipped from review as they are similar to previous changes (2)
- src/lib/redaction.ts
- supabase/functions/public-api/index.ts
| permissions: | ||
| contents: read | ||
| packages: write |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n .github/workflows/deploy-vps.ymlRepository: adm01-debug/zapp-web
Length of output: 3970
Escopo de packages: write está amplo demais.
Como permissions no topo vale para todos os jobs, o deploy herda packages: write sem precisar. O job apenas executa docker compose pull (read-only) e comandos remotos; não publica imagem. Mude a permissão para o job build-and-push e deixe o workflow em leitura, reduzindo o impacto de qualquer comprometimento no deploy.
Sugestão
permissions:
contents: read
- packages: write
jobs:
build-and-push:
name: Build and push image
runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
environment: ${{ inputs.environment }}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| permissions: | |
| contents: read | |
| packages: write | |
| permissions: | |
| contents: read | |
| jobs: | |
| build-and-push: | |
| name: Build and push image | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| environment: ${{ inputs.environment }} |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/deploy-vps.yml around lines 19 - 21, A permissão global
"packages: write" é demasiada — remova ou altere a entrada "packages: write" do
bloco permissions e mantenha apenas "contents: read" no topo; então adicionar
explicitamente "permissions: packages: write" apenas no bloco do job
"build-and-push" (onde as ações publicam imagens) e garanta que o job "deploy"
permaneça com leitura (ou sem escrever pacotes), já que este só executa "docker
compose pull" e comandos remotos; atualize a definição dos jobs referenciando o
nome "build-and-push" e verifique que não exista outro job que precise de write
global.
| host: ${{ secrets.VPS_HOST }} | ||
| username: ${{ secrets.VPS_USER }} | ||
| key: ${{ secrets.VPS_SSH_KEY }} | ||
| envs: ZAPP_WEB_IMAGE,ZAPP_WEB_HOST,ZAPP_WEB_DEPLOY_DIR | ||
| script: | |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
appleboy/ssh-action fingerprint parameter documentation
💡 Result:
In appleboy/ssh-action, the input parameter is named fingerprint. It is documented as: 1) Purpose The fingerprint input is used for SSH host key fingerprint verification to help prevent man-in-the-middle attacks. [1][2] 2) Expected value / format It expects the SHA256 fingerprint of the host public key. The README/CLI docs show computing it from the server’s host key file, extracting the fingerprint with cut -d ' ' -f2. [1][2] Example command (replace ed25519 with your key type and example.com with your host): ssh example.com ssh-keygen -l -f /etc/ssh/ssh_host_ed25519_key.pub | cut -d ' ' -f2 [1][2] 3) When it’s optional / default The documentation states the default behavior is to skip host fingerprint verification unless you provide the fingerprint value. [3] 4) Where it appears in configuration It’s shown in the action inputs example as fingerprint: ${{ secrets.FINGERPRINT }} alongside host, username, key, and port. [1][2] 5) Common pitfall: matching the correct host key type If your host has multiple host key types (rsa/ecdsa/ed25519), you must use the fingerprint corresponding to the host key type the SSH client negotiates; otherwise you can get a “host key fingerprint mismatch”. Community troubleshooting indicates that switching between ed25519/ecdsa/rsa fingerprints can make verification succeed. [4] So, the “fingerprint parameter documentation” answer: - Parameter name: fingerprint [1] - Description: SHA256 fingerprint of the host public key; used for host fingerprint verification to prevent MITM; default is to skip verification if not set. [1][3] - How to derive it: run ssh-keygen against the host’s /etc/ssh/ssh_host_<keytype>_key.pub file and extract the fingerprint field with cut -d ' ' -f2 (as shown in the docs). [1][2] - Provide the right key-type fingerprint to avoid mismatch errors. [4]
Citations:
- 1: https://github.com/appleboy/ssh-action
- 2: https://github.com/marketplace/actions/ssh-remote-commands
- 3: https://gitea.com/actions/appleboy-ssh-action/src/tag/v1.0.1
- 4: Host fingerprint verification fails appleboy/ssh-action#275
🏁 Script executed:
cat -n .github/workflows/deploy-vps.yml | sed -n '85,100p'Repository: adm01-debug/zapp-web
Length of output: 898
Adicionar pinning da identidade do host SSH no appleboy/ssh-action.
Sem o parâmetro fingerprint, o deploy conecta no servidor SSH sem validar sua chave pública, deixando aberta a porta para ataques de MITM. O appleboy/ssh-action@v1.2.0 suporta esse parâmetro nativamente — é direto adicionar:
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
+ fingerprint: ${{ secrets.VPS_HOST_FINGERPRINT }}
envs: ZAPP_WEB_IMAGE,ZAPP_WEB_HOST,ZAPP_WEB_DEPLOY_DIRExtraia o fingerprint do servidor com ssh-keygen -l -f /etc/ssh/ssh_host_<keytype>_key.pub | cut -d ' ' -f2 e armazene como secret.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/deploy-vps.yml around lines 91 - 95, Add SSH host key
pinning to the appleboy/ssh-action invocation by setting the fingerprint
parameter to the server's public key fingerprint (stored as a secret); update
the deploy workflow entry that uses appleboy/ssh-action@v1.2.0 (the block with
host, username, key, envs, script) to include fingerprint: ${{
secrets.VPS_SSH_FINGERPRINT }} so the action validates the server key before
connecting (generate and save the fingerprint using ssh-keygen as described and
add it to repository secrets).
| docker compose pull zapp-web || true | ||
| docker compose up -d zapp-web |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Primeiro, vamos encontrar e examinar o arquivo de workflow
find . -name "deploy-vps.yml" -type fRepository: adm01-debug/zapp-web
Length of output: 99
🏁 Script executed:
# Se o arquivo foi encontrado, vamos examinar seu conteúdo
cat -n .github/workflows/deploy-vps.yml | head -120Repository: adm01-debug/zapp-web
Length of output: 3970
🌐 Web query:
docker compose up --wait flag documentation behavior
💡 Result:
Behavior summary for docker compose up --wait (as of Docker Compose CLI docs): 1) What --wait does - docker compose up --wait makes Compose wait until services are “running|healthy” before the up command returns. It also implies detached mode (-d) [1]. 2) What it waits for (running vs healthy) - The docs describe it as “Wait for services to be running|healthy” [1]. In practice, this corresponds to Compose checking container state and/or healthcheck status to reach a suitable ready state. 3) Which services it waits for - When using --wait, Compose will wait for all services in the project to become running/healthy (not just dependency services). This is explicitly discussed as an intended effect: “up --wait makes compose wait for all services to be healthy, even for those not used as dependencies” [2]. 4) How it relates to healthchecks - If services have healthcheck configured, Compose uses that to determine the “healthy” state that --wait is waiting for [1]. 5) Timeout behavior - docker compose up also has --wait-timeout, described as “Maximum duration in seconds to wait for the project to be running|healthy” [1]. - Community reports indicate --wait-timeout is a maximum wait but Compose can stop waiting early if the engine reports a container as unhealthy (i.e., not just “keep waiting until the full timeout elapses”) [3]. 6) Exit/return-code expectations (important caveat) - --wait is not a general “wait until everything completes successfully” mechanism for short-lived jobs. - Compose expects services to stay running and become healthy; if containers exit (even successfully) while other services/jobs are involved, up --wait may still return a non-zero status because a “service” exited [4][5]. One issue report notes that init/setup containers exiting (0) can lead up --wait to exit with code 1 [4]. Another issue frames the limitation as: Compose doesn’t have an explicit concept for “jobs expected to exit” when using --wait [5]. Concrete takeaway for your wording - Use docker compose up --wait when you want your stack to be started and ready (running/healthy) before continuing, and expect the command to return while containers continue in the background (detached) [1]. - If you need to wait for one-off completion of tasks/jobs, --wait is not the right primitive; Compose issues report that it’s designed around services becoming running/healthy rather than jobs exiting successfully [4][5].
Citations:
- 1: https://docs.docker.com/reference/cli/docker/compose/up/
- 2: [BUG] docker compose up -d waits even without --wait docker/compose#13426
- 3: [BUG] docker-compose up --wait --wait-timeout 300 docker/compose#12416
- 4: [BUG] compose up --wait exits 1 on init containers successfully completing docker/compose#10596
- 5: [BUG]
docker compose up -d waitincorrectly returning1when all services/jobs reported healthy docker/compose#13069
O workflow conclui com sucesso antes do serviço estar saudável.
docker compose up -d retorna imediatamente, deixando o container iniciando em background. Sem esperar explicitamente, o job marca sucesso enquanto o zapp-web ainda está subindo, causando falsos positivos de deploy bem-sucedido.
🔧 Solução
- docker compose up -d zapp-web
+ docker compose up -d --wait --wait-timeout 60 zapp-web📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| docker compose pull zapp-web || true | |
| docker compose up -d zapp-web | |
| docker compose pull zapp-web || true | |
| docker compose up -d --wait --wait-timeout 60 zapp-web |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/deploy-vps.yml around lines 100 - 101, The deploy job
currently runs "docker compose up -d zapp-web" and exits before the service is
healthy; modify the workflow to wait for the container to reach a healthy state
after "docker compose up -d zapp-web" by polling the service health (e.g., use
"docker compose ps" or "docker inspect" on the "zapp-web" container ID or run
repeated HTTP probes against the app) and only exit successfully once the health
status is "healthy" or the HTTP probe returns a 2xx/3xx; fail the step if a
configurable timeout is exceeded. Ensure the wait logic is implemented
immediately after the existing "docker compose up -d zapp-web" command so the
job reflects the actual service readiness.
| CREATE UNIQUE INDEX IF NOT EXISTS idx_messages_request_id_unique | ||
| ON public.messages (request_id) | ||
| WHERE request_id IS NOT NULL AND request_id <> ''; |
There was a problem hiding this comment.
Criação de índice único pode quebrar a migration com dados legados duplicados.
Na Line 8, CREATE UNIQUE INDEX vai falhar se já existir request_id duplicado (cenário possível antes da proteção no banco), interrompendo deploy/CI.
💡 Patch mínimo para falhar cedo com erro explícito
+DO $$
+BEGIN
+ IF EXISTS (
+ SELECT 1
+ FROM public.messages
+ WHERE request_id IS NOT NULL AND request_id <> ''
+ GROUP BY request_id
+ HAVING COUNT(*) > 1
+ ) THEN
+ RAISE EXCEPTION 'Duplicatas em public.messages.request_id; dedupe antes de criar idx_messages_request_id_unique';
+ END IF;
+END $$;
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_messages_request_id_unique
ON public.messages (request_id)
WHERE request_id IS NOT NULL AND request_id <> '';📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| CREATE UNIQUE INDEX IF NOT EXISTS idx_messages_request_id_unique | |
| ON public.messages (request_id) | |
| WHERE request_id IS NOT NULL AND request_id <> ''; | |
| DO $$ | |
| BEGIN | |
| IF EXISTS ( | |
| SELECT 1 | |
| FROM public.messages | |
| WHERE request_id IS NOT NULL AND request_id <> '' | |
| GROUP BY request_id | |
| HAVING COUNT(*) > 1 | |
| ) THEN | |
| RAISE EXCEPTION 'Duplicatas em public.messages.request_id; dedupe antes de criar idx_messages_request_id_unique'; | |
| END IF; | |
| END $$; | |
| CREATE UNIQUE INDEX IF NOT EXISTS idx_messages_request_id_unique | |
| ON public.messages (request_id) | |
| WHERE request_id IS NOT NULL AND request_id <> ''; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@supabase/migrations/20260513113000_pr132_review_hardening.sql` around lines 8
- 10, The CREATE UNIQUE INDEX idx_messages_request_id_unique on
public.messages(request_id) will fail if legacy duplicate request_id values
exist; before creating the unique index add a pre-check that queries
public.messages grouped by request_id to detect duplicates and raise a clear
error (or abort the migration) if any duplicate count > 1, so the migration
fails early with an explicit message guiding remediation (or alternatively
deduplicate/update those rows prior to creating idx_messages_request_id_unique).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ad43043f6a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| add_header X-Frame-Options "SAMEORIGIN" always; | ||
| add_header X-Content-Type-Options "nosniff" always; | ||
| add_header Referrer-Policy "strict-origin-when-cross-origin" always; | ||
| add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always; |
There was a problem hiding this comment.
Allow same-origin microphone access
In VPS/Docker deployments this header is sent on every route, and microphone=() disables microphone access for the page. The app still has production flows that call navigator.mediaDevices.getUserMedia({ audio: ... }) (for example src/hooks/useAudioRecorder.ts and src/features/inbox/components/VoiceChangerPicker.tsx), so voice notes/recording fail only after this nginx config is used. Use a same-origin microphone policy instead of denying it globally.
Useful? React with 👍 / 👎.
| CREATE UNIQUE INDEX IF NOT EXISTS idx_messages_request_id_unique | ||
| ON public.messages (request_id) | ||
| WHERE request_id IS NOT NULL AND request_id <> ''; |
There was a problem hiding this comment.
Keep tracing IDs separate from idempotency keys
This unique index makes every non-empty messages.request_id globally unique, but request_id is also used for ordinary tracing: the shared Edge Logger honors inbound x-request-id, and public-api still stamps traceId = idempotencyKey ?? requestId. If a client reuses one x-request-id to correlate multiple sends without an idempotency key, the second insert hits this constraint and returns Failed to save message instead of sending. Scope uniqueness to a dedicated idempotency column/key or only stamp request_id with the idempotency key when idempotency is requested.
Useful? React with 👍 / 👎.
| - name: Generate RLS Report | ||
| run: bun scripts/verify_rls_compliance.ts > rls-compliance-report.md | ||
| - name: Publish Weekly Compliance | ||
| run: bun run security:rls | tee rls-compliance-report.md |
There was a problem hiding this comment.
Preserve RLS audit failures through tee
When bun run security:rls detects non-compliant tables it exits non-zero, but this step pipes it to tee without enabling pipefail; in bash the pipeline status is the last command's status, so a successful tee makes the workflow pass while only uploading the failure report. Add set -o pipefail (or redirect output without a pipeline) so PRs/pushes are actually blocked by the RLS gate.
Useful? React with 👍 / 👎.
|
|
||
| location / { | ||
| try_files $uri $uri/ /index.html; | ||
| add_header Cache-Control "no-cache" always; |
There was a problem hiding this comment.
Reapply security headers inside nginx locations
With nginx, add_header directives from the server block are inherited only by locations that define no add_header of their own. Because this SPA location adds Cache-Control, responses for / and the /index.html fallback no longer include the X-Frame-Options, nosniff, Referrer-Policy, or Permissions-Policy headers configured above in VPS/Docker deployments. Repeat those headers in the locations (or otherwise merge inheritance) so the main app shell keeps the intended security headers.
Useful? React with 👍 / 👎.
|
Atualizei novamente o PR com o commit
Validações executadas localmente: Observação: não executei comandos diretamente na produção/VPS porque esta sessão não expõe Portainer/Supabase MCP nem Docker Swarm remoto; deixei os scripts e o runbook prontos para execução segura no ambiente operacional. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0ad35b5086
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| .replace(EMAIL_PATTERN, REDACTED) | ||
| .replace(CPF_CNPJ_PATTERN, REDACTED) | ||
| .replace(BRAZIL_PHONE_PATTERN, REDACTED) | ||
| .replace(WHATSAPP_JID_PATTERN, `${REDACTED}@$1`) |
There was a problem hiding this comment.
Align WhatsApp JID redaction with the new test
With the redaction test added in src/lib/redaction.test.ts, a WhatsApp JID such as 5511999999999@s.whatsapp.net is expected to become just [REDACTED], but this replacement preserves the domain and produces [REDACTED]@s.whatsapp.net. That makes the newly added Vitest case fail in CI for any run that includes src/lib/redaction.test.ts; either drop the suffix here or update the test/contract to match the intended output.
Useful? React with 👍 / 👎.
| "payload": { | ||
| "type": "object", | ||
| "additionalProperties": true | ||
| }, |
There was a problem hiding this comment.
Fix the public-api request schema
The new OpenAPI contract for /public-api does not match the handler: supabase/functions/_shared/criticalPayloadSchemas.ts requires top-level action: 'send', number, and message (plus optional connectionId), but this schema only permits action, payload, and requestId with additionalProperties: false. Any client generated or validated from this contract will reject the actual valid send body and may send a payload wrapper that the Edge Function returns as a 400.
Useful? React with 👍 / 👎.
| "hmacSignature": { | ||
| "type": "apiKey", | ||
| "in": "header", | ||
| "name": "x-signature" |
There was a problem hiding this comment.
Use the WhatsApp Cloud signature header
This shared HMAC security scheme documents x-signature, but supabase/functions/whatsapp-cloud-webhook/index.ts only reads x-hub-signature-256 before calling verifyHmacSignature. Clients generated from this OpenAPI spec will sign WhatsApp Cloud webhook requests into the documented header and still receive 401s whenever the app secret/strict signature validation is enabled, so this endpoint needs its actual header documented separately.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
scripts/check-supabase-drift.mjs (1)
64-66: ⚡ Quick winTratamento de erro no
--inspect-jsonpode ser mais resiliente.No modo offline, arquivo ausente/JSON inválido derruba com exceção crua. Vale retornar erro operacional explícito.
💡 Ajuste sugerido
function readRuntimeImages() { if (inspectFile) { - return JSON.parse(readFileSync(inspectFile, 'utf8')); + if (!existsSync(inspectFile)) { + console.error(`❌ Inspect JSON not found: ${inspectFile}`); + process.exit(1); + } + try { + return JSON.parse(readFileSync(inspectFile, 'utf8')); + } catch (error) { + console.error(`❌ Invalid inspect JSON: ${inspectFile}`); + process.exit(1); + } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/check-supabase-drift.mjs` around lines 64 - 66, O código que usa inspectFile atualmente retorna JSON.parse(readFileSync(...)) sem validação, então arquivo ausente ou JSON inválido lança uma exceção cru; envolva a leitura/parse num bloco try/catch, valide que inspectFile exista antes de ler, capture erros de leitura ou JSON.parse e lance/retorne um erro operacional claro (ex.: "failed to load --inspect-json: file not found" ou "invalid JSON in --inspect-json") incluindo o nome do arquivo; refira-se às chamadas inspectFile, readFileSync e JSON.parse para localizar onde adicionar a verificação e o try/catch.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@scripts/check-supabase-drift.mjs`:
- Around line 89-97: O problema é que a linha que calcula image usa
.split('@')[0], removendo o digest e causando falsos drifts; ajuste para
preservar o digest ou normalizar ambos os lados antes da comparação: in the
block that sets runtime (variable image and call runtime.set(name, image)) stop
truncating at '@' (keep the full ContainerSpec.Image) or apply the same
normalization to the expected map (variables expected / expectedImage) so
runtimeImage === expectedImage compares equivalent forms; update the code that
builds rows (runtimeImage, expectedImage, pass) to use the canonical form (full
image with digest or both stripped consistently) so comparisons are accurate.
In `@scripts/manual-supabase-backup.sh`:
- Around line 68-71: A validação atual que só checa IPv4 (a condicional que
testa "$PGHOST" com regex [0-9]+\.[0-9]+\.[0-9]+\.[0-9]+) permite literais IPv6;
atualize essa verificação para também detectar endereços IPv6 literais (por
exemplo verificando presença de ':' ou usando uma regex/expressão para IPv6) e
fazer o mesmo erro/exit quando PGHOST for um literal IP; ajuste a condicional
que envolve a variável PGHOST para cobrir ambos os formatos e mantenha a mesma
mensagem de erro e exit code.
---
Nitpick comments:
In `@scripts/check-supabase-drift.mjs`:
- Around line 64-66: O código que usa inspectFile atualmente retorna
JSON.parse(readFileSync(...)) sem validação, então arquivo ausente ou JSON
inválido lança uma exceção cru; envolva a leitura/parse num bloco try/catch,
valide que inspectFile exista antes de ler, capture erros de leitura ou
JSON.parse e lance/retorne um erro operacional claro (ex.: "failed to load
--inspect-json: file not found" ou "invalid JSON in --inspect-json") incluindo o
nome do arquivo; refira-se às chamadas inspectFile, readFileSync e JSON.parse
para localizar onde adicionar a verificação e o try/catch.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 4eb9ffde-dd91-464b-b452-6b30d5182be7
📒 Files selected for processing (5)
docs/operations/supabase-backup-and-drift-runbook.mdpackage.jsonscripts/check-supabase-drift.mjsscripts/manual-supabase-backup.shscripts/validate-supabase-backup-artifact.mjs
🚧 Files skipped from review as they are similar to previous changes (1)
- package.json
| const image = service?.Spec?.TaskTemplate?.ContainerSpec?.Image?.split('@')[0]; | ||
| if (name && image) runtime.set(name, image); | ||
| } | ||
|
|
||
| const rows = []; | ||
| for (const [service, expectedImage] of expected) { | ||
| const runtimeImage = runtime.get(service); | ||
| rows.push({ service, expectedImage, runtimeImage, pass: runtimeImage === expectedImage }); | ||
| } |
There was a problem hiding this comment.
Comparação de imagem falha com referências pinadas por digest.
A runtime image é truncada antes da comparação. Se o compose declarar image@sha256:..., o script pode sinalizar drift falso mesmo com serviço correto.
💡 Ajuste sugerido
+function comparableImage(expectedImage, runtimeImage) {
+ const expectedHasDigest = expectedImage.includes('@');
+ if (!runtimeImage) return { expectedComparable: expectedImage, runtimeComparable: runtimeImage };
+ if (expectedHasDigest) return { expectedComparable: expectedImage, runtimeComparable: runtimeImage };
+ return {
+ expectedComparable: expectedImage.split('@')[0],
+ runtimeComparable: runtimeImage.split('@')[0],
+ };
+}
+
const runtime = new Map();
for (const service of inspected) {
const name = service?.Spec?.Name?.startsWith(`${stack}_`)
? service.Spec.Name.slice(stack.length + 1)
: service?.Spec?.Name;
- const image = service?.Spec?.TaskTemplate?.ContainerSpec?.Image?.split('@')[0];
+ const image = service?.Spec?.TaskTemplate?.ContainerSpec?.Image;
if (name && image) runtime.set(name, image);
}
const rows = [];
for (const [service, expectedImage] of expected) {
const runtimeImage = runtime.get(service);
- rows.push({ service, expectedImage, runtimeImage, pass: runtimeImage === expectedImage });
+ const { expectedComparable, runtimeComparable } = comparableImage(expectedImage, runtimeImage);
+ rows.push({ service, expectedImage, runtimeImage, pass: runtimeComparable === expectedComparable });
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/check-supabase-drift.mjs` around lines 89 - 97, O problema é que a
linha que calcula image usa .split('@')[0], removendo o digest e causando falsos
drifts; ajuste para preservar o digest ou normalizar ambos os lados antes da
comparação: in the block that sets runtime (variable image and call
runtime.set(name, image)) stop truncating at '@' (keep the full
ContainerSpec.Image) or apply the same normalization to the expected map
(variables expected / expectedImage) so runtimeImage === expectedImage compares
equivalent forms; update the code that builds rows (runtimeImage, expectedImage,
pass) to use the canonical form (full image with digest or both stripped
consistently) so comparisons are accurate.
| if [[ "$PGHOST" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | ||
| echo "❌ PGHOST is an IP address (${PGHOST}). Use stable DNS such as supabase_db." >&2 | ||
| exit 2 | ||
| fi |
There was a problem hiding this comment.
Validação de PGHOST ainda permite IP em formato IPv6.
A checagem atual bloqueia só IPv4; um host IPv6 literal passa e quebra a regra operacional de usar DNS estável no Swarm.
💡 Ajuste sugerido
-if [[ "$PGHOST" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+if [[ "$PGHOST" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ || "$PGHOST" == *:* ]]; then
echo "❌ PGHOST is an IP address (${PGHOST}). Use stable DNS such as supabase_db." >&2
exit 2
fi📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if [[ "$PGHOST" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| echo "❌ PGHOST is an IP address (${PGHOST}). Use stable DNS such as supabase_db." >&2 | |
| exit 2 | |
| fi | |
| if [[ "$PGHOST" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ || "$PGHOST" == *:* ]]; then | |
| echo "❌ PGHOST is an IP address (${PGHOST}). Use stable DNS such as supabase_db." >&2 | |
| exit 2 | |
| fi |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/manual-supabase-backup.sh` around lines 68 - 71, A validação atual
que só checa IPv4 (a condicional que testa "$PGHOST" com regex
[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+) permite literais IPv6; atualize essa verificação
para também detectar endereços IPv6 literais (por exemplo verificando presença
de ':' ou usando uma regex/expressão para IPv6) e fazer o mesmo erro/exit quando
PGHOST for um literal IP; ajuste a condicional que envolve a variável PGHOST
para cobrir ambos os formatos e mantenha a mesma mensagem de erro e exit code.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
10 issues found across 32 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="scripts/validate-openapi.mjs">
<violation number="1" location="scripts/validate-openapi.mjs:39">
P2: The validator incorrectly requires `requestBody` for every non-GET operation, causing false CI failures for valid endpoints such as `DELETE` without a body.</violation>
<violation number="2" location="scripts/validate-openapi.mjs:45">
P2: Webhook header validation is overly coupled to component naming and can reject valid specs that define the required headers inline.</violation>
</file>
<file name=".github/workflows/deploy-vps.yml">
<violation number="1" location=".github/workflows/deploy-vps.yml:97">
P2: The deploy script ignores `ZAPP_WEB_DEPLOY_DIR` and hardcodes `/opt/zapp-web`, which can fail deployments in environments configured with a different path.</violation>
<violation number="2" location=".github/workflows/deploy-vps.yml:100">
P2: Do not swallow `docker compose pull` failures; this can silently deploy a stale image while the workflow still passes.</violation>
</file>
<file name="scripts/manual-supabase-backup.sh">
<violation number="1" location="scripts/manual-supabase-backup.sh:54">
P1: A failure in optional `BACKUP_COPY_DIR` copy can delete an already successful primary backup because the cleanup trap is still active.</violation>
</file>
<file name="nginx.conf">
<violation number="1" location="nginx.conf:49">
P2: Adding `add_header` inside `location` blocks drops inherited security headers on those responses. As written, responses from `location /` won’t include the server-level hardening headers.</violation>
</file>
<file name="scripts/check-performance-budget.mjs">
<violation number="1" location="scripts/check-performance-budget.mjs:11">
P1: Valide os valores de budget das variáveis de ambiente; valores não numéricos viram `NaN` e podem desativar silenciosamente as verificações de performance.</violation>
</file>
<file name="src/lib/redaction.test.ts">
<violation number="1" location="src/lib/redaction.test.ts:28">
P2: This assertion does not match the current JID redaction behavior and will fail in CI.</violation>
</file>
<file name=".github/workflows/security.yml">
<violation number="1" location=".github/workflows/security.yml:61">
P1: The RLS check can fail silently because this piped command runs without `pipefail` by default.</violation>
</file>
<file name="docker-compose.yml">
<violation number="1" location="docker-compose.yml:6">
P2: Required `${...:?required}` build args can break image-only VPS deploys because Compose fails during file interpolation before startup.</violation>
</file>
Tip: instead of fixing issues one by one fix them all with cubic
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
| SHA_FILE="${BACKUP_FILE}.sha256" | ||
|
|
||
| mkdir -p "$BACKUP_DIR" | ||
| if [[ -n "${BACKUP_COPY_DIR:-}" ]]; then |
There was a problem hiding this comment.
P1: A failure in optional BACKUP_COPY_DIR copy can delete an already successful primary backup because the cleanup trap is still active.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/manual-supabase-backup.sh, line 54:
<comment>A failure in optional `BACKUP_COPY_DIR` copy can delete an already successful primary backup because the cleanup trap is still active.</comment>
<file context>
@@ -0,0 +1,105 @@
+SHA_FILE="${BACKUP_FILE}.sha256"
+
+mkdir -p "$BACKUP_DIR"
+if [[ -n "${BACKUP_COPY_DIR:-}" ]]; then
+ mkdir -p "$BACKUP_COPY_DIR"
+fi
</file context>
| initialJsGzipKb: Number(process.env.PERF_BUDGET_INITIAL_JS_GZIP_KB ?? 1500), | ||
| chunkJsGzipKb: Number(process.env.PERF_BUDGET_CHUNK_JS_GZIP_KB ?? 1500), | ||
| totalJsGzipKb: Number(process.env.PERF_BUDGET_TOTAL_JS_GZIP_KB ?? 2700), | ||
| totalCssGzipKb: Number(process.env.PERF_BUDGET_TOTAL_CSS_GZIP_KB ?? 250), |
There was a problem hiding this comment.
P1: Valide os valores de budget das variáveis de ambiente; valores não numéricos viram NaN e podem desativar silenciosamente as verificações de performance.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/check-performance-budget.mjs, line 11:
<comment>Valide os valores de budget das variáveis de ambiente; valores não numéricos viram `NaN` e podem desativar silenciosamente as verificações de performance.</comment>
<file context>
@@ -0,0 +1,87 @@
+const assetsDir = join(distDir, 'assets');
+
+const budgets = {
+ initialJsGzipKb: Number(process.env.PERF_BUDGET_INITIAL_JS_GZIP_KB ?? 1500),
+ chunkJsGzipKb: Number(process.env.PERF_BUDGET_CHUNK_JS_GZIP_KB ?? 1500),
+ totalJsGzipKb: Number(process.env.PERF_BUDGET_TOTAL_JS_GZIP_KB ?? 2700),
</file context>
| initialJsGzipKb: Number(process.env.PERF_BUDGET_INITIAL_JS_GZIP_KB ?? 1500), | |
| chunkJsGzipKb: Number(process.env.PERF_BUDGET_CHUNK_JS_GZIP_KB ?? 1500), | |
| totalJsGzipKb: Number(process.env.PERF_BUDGET_TOTAL_JS_GZIP_KB ?? 2700), | |
| totalCssGzipKb: Number(process.env.PERF_BUDGET_TOTAL_CSS_GZIP_KB ?? 250), | |
| initialJsGzipKb: Number.isFinite(Number(process.env.PERF_BUDGET_INITIAL_JS_GZIP_KB)) ? Number(process.env.PERF_BUDGET_INITIAL_JS_GZIP_KB) : 1500, | |
| chunkJsGzipKb: Number.isFinite(Number(process.env.PERF_BUDGET_CHUNK_JS_GZIP_KB)) ? Number(process.env.PERF_BUDGET_CHUNK_JS_GZIP_KB) : 1500, | |
| totalJsGzipKb: Number.isFinite(Number(process.env.PERF_BUDGET_TOTAL_JS_GZIP_KB)) ? Number(process.env.PERF_BUDGET_TOTAL_JS_GZIP_KB) : 2700, | |
| totalCssGzipKb: Number.isFinite(Number(process.env.PERF_BUDGET_TOTAL_CSS_GZIP_KB)) ? Number(process.env.PERF_BUDGET_TOTAL_CSS_GZIP_KB) : 250, |
| - name: Generate RLS Report | ||
| run: bun scripts/verify_rls_compliance.ts > rls-compliance-report.md | ||
| - name: Publish Weekly Compliance | ||
| run: bun run security:rls | tee rls-compliance-report.md |
There was a problem hiding this comment.
P1: The RLS check can fail silently because this piped command runs without pipefail by default.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/security.yml, line 61:
<comment>The RLS check can fail silently because this piped command runs without `pipefail` by default.</comment>
<file context>
@@ -35,25 +35,33 @@ jobs:
- name: Generate RLS Report
- run: bun scripts/verify_rls_compliance.ts > rls-compliance-report.md
- - name: Publish Weekly Compliance
+ run: bun run security:rls | tee rls-compliance-report.md
+ - name: Publish RLS Compliance
uses: actions/upload-artifact@v4
</file context>
| run: bun run security:rls | tee rls-compliance-report.md | |
| run: | | |
| set -o pipefail | |
| bun run security:rls | tee rls-compliance-report.md |
| if (path.includes('webhook') && method === 'post') { | ||
| const securityNames = (operation.security ?? []).flatMap((entry) => Object.keys(entry)); | ||
| if (!securityNames.includes('hmacSignature')) fail(`${label} webhook POST must require hmacSignature`); | ||
| const parameterRefs = (operation.parameters ?? []).map((param) => param.$ref ?? param.name); |
There was a problem hiding this comment.
P2: Webhook header validation is overly coupled to component naming and can reject valid specs that define the required headers inline.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/validate-openapi.mjs, line 45:
<comment>Webhook header validation is overly coupled to component naming and can reject valid specs that define the required headers inline.</comment>
<file context>
@@ -0,0 +1,58 @@
+ if (path.includes('webhook') && method === 'post') {
+ const securityNames = (operation.security ?? []).flatMap((entry) => Object.keys(entry));
+ if (!securityNames.includes('hmacSignature')) fail(`${label} webhook POST must require hmacSignature`);
+ const parameterRefs = (operation.parameters ?? []).map((param) => param.$ref ?? param.name);
+ if (!parameterRefs.some((ref) => String(ref).includes('RequestIdHeader'))) fail(`${label} webhook POST must document x-request-id`);
+ if (!parameterRefs.some((ref) => String(ref).includes('IdempotencyKeyHeader'))) fail(`${label} webhook POST must document idempotency header`);
</file context>
| if (!operation.responses || Object.keys(operation.responses).length === 0) fail(`${label} missing responses`); | ||
| const successResponse = Object.keys(operation.responses ?? {}).find((status) => /^2\d\d$/.test(status)); | ||
| if (!successResponse) fail(`${label} missing 2xx response`); | ||
| if (method !== 'get' && !operation.requestBody && !path.includes('webhook')) { |
There was a problem hiding this comment.
P2: The validator incorrectly requires requestBody for every non-GET operation, causing false CI failures for valid endpoints such as DELETE without a body.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/validate-openapi.mjs, line 39:
<comment>The validator incorrectly requires `requestBody` for every non-GET operation, causing false CI failures for valid endpoints such as `DELETE` without a body.</comment>
<file context>
@@ -0,0 +1,58 @@
+ if (!operation.responses || Object.keys(operation.responses).length === 0) fail(`${label} missing responses`);
+ const successResponse = Object.keys(operation.responses ?? {}).find((status) => /^2\d\d$/.test(status));
+ if (!successResponse) fail(`${label} missing 2xx response`);
+ if (method !== 'get' && !operation.requestBody && !path.includes('webhook')) {
+ fail(`${label} non-GET operation should declare requestBody`);
+ }
</file context>
| if (method !== 'get' && !operation.requestBody && !path.includes('webhook')) { | |
| if (['post', 'put', 'patch'].includes(method) && !operation.requestBody && !path.includes('webhook')) { |
| envs: ZAPP_WEB_IMAGE,ZAPP_WEB_HOST,ZAPP_WEB_DEPLOY_DIR | ||
| script: | | ||
| set -euo pipefail | ||
| cd "/opt/zapp-web" |
There was a problem hiding this comment.
P2: The deploy script ignores ZAPP_WEB_DEPLOY_DIR and hardcodes /opt/zapp-web, which can fail deployments in environments configured with a different path.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/deploy-vps.yml, line 97:
<comment>The deploy script ignores `ZAPP_WEB_DEPLOY_DIR` and hardcodes `/opt/zapp-web`, which can fail deployments in environments configured with a different path.</comment>
<file context>
@@ -0,0 +1,102 @@
+ envs: ZAPP_WEB_IMAGE,ZAPP_WEB_HOST,ZAPP_WEB_DEPLOY_DIR
+ script: |
+ set -euo pipefail
+ cd "/opt/zapp-web"
+ export ZAPP_WEB_IMAGE="$ZAPP_WEB_IMAGE"
+ export ZAPP_WEB_HOST="${ZAPP_WEB_HOST:-zapp.atomicabr.com.br}"
</file context>
| cd "/opt/zapp-web" | |
| cd "${ZAPP_WEB_DEPLOY_DIR:-/opt/zapp-web}" |
| cd "/opt/zapp-web" | ||
| export ZAPP_WEB_IMAGE="$ZAPP_WEB_IMAGE" | ||
| export ZAPP_WEB_HOST="${ZAPP_WEB_HOST:-zapp.atomicabr.com.br}" | ||
| docker compose pull zapp-web || true |
There was a problem hiding this comment.
P2: Do not swallow docker compose pull failures; this can silently deploy a stale image while the workflow still passes.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/deploy-vps.yml, line 100:
<comment>Do not swallow `docker compose pull` failures; this can silently deploy a stale image while the workflow still passes.</comment>
<file context>
@@ -0,0 +1,102 @@
+ cd "/opt/zapp-web"
+ export ZAPP_WEB_IMAGE="$ZAPP_WEB_IMAGE"
+ export ZAPP_WEB_HOST="${ZAPP_WEB_HOST:-zapp.atomicabr.com.br}"
+ docker compose pull zapp-web || true
+ docker compose up -d zapp-web
+ docker image prune -f
</file context>
| docker compose pull zapp-web || true | |
| docker compose pull zapp-web |
|
|
||
| location / { | ||
| try_files $uri $uri/ /index.html; | ||
| add_header Cache-Control "no-cache" always; |
There was a problem hiding this comment.
P2: Adding add_header inside location blocks drops inherited security headers on those responses. As written, responses from location / won’t include the server-level hardening headers.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At nginx.conf, line 49:
<comment>Adding `add_header` inside `location` blocks drops inherited security headers on those responses. As written, responses from `location /` won’t include the server-level hardening headers.</comment>
<file context>
@@ -0,0 +1,51 @@
+
+ location / {
+ try_files $uri $uri/ /index.html;
+ add_header Cache-Control "no-cache" always;
+ }
+}
</file context>
| 'Bearer abc.def.ghi CPF 123.456.789-10 tel +55 11 98765-4321 jid 5511999999999@s.whatsapp.net'; | ||
|
|
||
| expect(redactSensitiveData(input)).toBe( | ||
| 'Bearer [REDACTED] CPF [REDACTED] tel [REDACTED] jid [REDACTED]' |
There was a problem hiding this comment.
P2: This assertion does not match the current JID redaction behavior and will fail in CI.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/lib/redaction.test.ts, line 28:
<comment>This assertion does not match the current JID redaction behavior and will fail in CI.</comment>
<file context>
@@ -0,0 +1,45 @@
+ 'Bearer abc.def.ghi CPF 123.456.789-10 tel +55 11 98765-4321 jid 5511999999999@s.whatsapp.net';
+
+ expect(redactSensitiveData(input)).toBe(
+ 'Bearer [REDACTED] CPF [REDACTED] tel [REDACTED] jid [REDACTED]'
+ );
+ });
</file context>
| build: | ||
| context: . | ||
| args: | ||
| VITE_SUPABASE_URL: ${VITE_SUPABASE_URL:?required} |
There was a problem hiding this comment.
P2: Required ${...:?required} build args can break image-only VPS deploys because Compose fails during file interpolation before startup.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docker-compose.yml, line 6:
<comment>Required `${...:?required}` build args can break image-only VPS deploys because Compose fails during file interpolation before startup.</comment>
<file context>
@@ -0,0 +1,34 @@
+ build:
+ context: .
+ args:
+ VITE_SUPABASE_URL: ${VITE_SUPABASE_URL:?required}
+ VITE_SUPABASE_ANON_KEY: ${VITE_SUPABASE_ANON_KEY:?required}
+ VITE_EXTERNAL_SUPABASE_URL: ${VITE_EXTERNAL_SUPABASE_URL:-${VITE_SUPABASE_URL}}
</file context>
…148) - Dockerfile, docker-compose.yml, nginx.conf, .dockerignore: runtime container para VPS - .github/workflows/deploy-vps.yml: pipeline manual de build/push + SSH restart - .github/workflows/security.yml: dependency-review no PR + rls-audit melhorado - .github/workflows/ci.yml: typecheck:strict:core, check:domain, check:barrels, api:validate, vps:check, perf:budget - docs/api/openapi.json + scripts/validate-openapi.mjs: contrato Edge Functions validado no CI - src/lib/redaction.ts + logger.ts: redação automática de secrets/PII em logs - supabase/functions/public-api/index.ts: idempotência com duplicate detection e trace stamping - scripts/{check-performance-budget,check-supabase-drift,check-vps-readiness,manual-supabase-backup,validate-openapi,validate-supabase-backup-artifact}.mjs - supabase/migrations: rls_compliance_gap_closure + pr132_review_hardening - tsconfig.strict.core.json: typecheck estrito para módulos core - docs: observabilidade/SLOs, segurança/auth-baseline, operações/backup-runbook, auditoria técnica Closes #132 Co-authored-by: Claude <noreply@anthropic.com>
|
Substituída por #148 — versão limpa rebaseada no main atual. Arquivos conflitantes ( Generated by Claude Code |
Motivation
Description
.github/workflows/ci.ymlwithtypecheck:strict:core, domain/barrel checks,api:validate,vps:checkandperf:budget; added.github/workflows/deploy-vps.ymlfor manual VPS image build/push + SSH restart.Dockerfile,docker-compose.yml,.dockerignore,nginx.conf,public/healthzandpublic/version.jsonto support containerized VPS deploys.docs/api/openapi.jsonandscripts/validate-openapi.mjsto validate Edge Functions contract againstsupabase/functions.scripts/verify_rls_compliance.tsand migrationsupabase/migrations/20260513093000_rls_compliance_gap_closure.sqlto close static RLS gaps; updated security workflow to run dependency review on PRs and publish RLS reports.docs/observability/slos.md,docs/security/auth-baseline.md, changelog entry and long-form technical audit.scripts/check-performance-budget.mjsand integratedperf:budgetintopackage.jsoncheckscript and CI.tsconfig.strict.core.json, updatedvite.config.tsto settarget,manualChunksand chunk warning limits, and updated package scripts to include new checks.src/lib/redaction.ts, wired intosrc/lib/logger.ts, and added unit testssrc/lib/redaction.test.ts.supabase/functions/public-api/index.tsto support idempotency headers, duplicate detection, and trace stamping.Testing
npm run api:validatenpm run vps:checknpm run perf:budgetbun run scripts/verify_rls_compliance.tsbunx vitest run src/lib/redaction.test.tsnpm run buildNotes
npm run buildcompleted successfully; local prebuild emitted existing environment warnings because Supabase project/secrets/Docker are unavailable in this container.Summary by CodeRabbit
New Features
Documentation
Chores
Tests