Skip to content

Add CI gates, OpenAPI & RLS validation, VPS deploy, logger redaction and idempotency support#132

Closed
adm01-debug wants to merge 9 commits into
mainfrom
codex/ci-openapi-rls-vps-hardening
Closed

Add CI gates, OpenAPI & RLS validation, VPS deploy, logger redaction and idempotency support#132
adm01-debug wants to merge 9 commits into
mainfrom
codex/ci-openapi-rls-vps-hardening

Conversation

@adm01-debug
Copy link
Copy Markdown
Owner

@adm01-debug adm01-debug commented May 13, 2026

Motivation

  • Harden CI and release readiness by adding stricter typechecks, domain/barrel checks, OpenAPI validation, performance budget and VPS readiness gating.
  • Improve security/compliance posture with automated RLS auditing and dependency review in PRs.
  • Prepare a reproducible VPS deploy flow and container runtime for production/staging.
  • Reduce accidental leakage of secrets/PII in logs and make public Edge APIs safer under retries via idempotency.

Description

  • CI/workflows: extended .github/workflows/ci.yml with typecheck:strict:core, domain/barrel checks, api:validate, vps:check and perf:budget; added .github/workflows/deploy-vps.yml for manual VPS image build/push + SSH restart.
  • Infra & runtime: added Dockerfile, docker-compose.yml, .dockerignore, nginx.conf, public/healthz and public/version.json to support containerized VPS deploys.
  • OpenAPI & contracts: added docs/api/openapi.json and scripts/validate-openapi.mjs to validate Edge Functions contract against supabase/functions.
  • RLS & compliance: added scripts/verify_rls_compliance.ts and migration supabase/migrations/20260513093000_rls_compliance_gap_closure.sql to close static RLS gaps; updated security workflow to run dependency review on PRs and publish RLS reports.
  • Observability & ops docs: added docs/observability/slos.md, docs/security/auth-baseline.md, changelog entry and long-form technical audit.
  • Performance gating: added scripts/check-performance-budget.mjs and integrated perf:budget into package.json check script and CI.
  • Typing & build: added tsconfig.strict.core.json, updated vite.config.ts to set target, manualChunks and chunk warning limits, and updated package scripts to include new checks.
  • Logging/redaction & tests: added src/lib/redaction.ts, wired into src/lib/logger.ts, and added unit tests src/lib/redaction.test.ts.
  • Public API resilience: updated supabase/functions/public-api/index.ts to support idempotency headers, duplicate detection, and trace stamping.

Testing

  • npm run api:validate
  • npm run vps:check
  • npm run perf:budget
  • bun run scripts/verify_rls_compliance.ts
  • bunx vitest run src/lib/redaction.test.ts
  • npm run build

Notes

  • npm run build completed successfully; local prebuild emitted existing environment warnings because Supabase project/secrets/Docker are unavailable in this container.

Summary by CodeRabbit

  • New Features

    • Endpoints de health/version, idempotência no public API, redaction automática de dados sensíveis e opção de deploy via VPS.
  • Documentation

    • OpenAPI pública, SLOs/observabilidade, auditoria técnica, baseline de segurança e runbooks operacionais.
  • Chores

    • CI ampliado com validações e verificação de performance; reforço de RLS e índice de idempotência no banco; imagens/container/nginx e versão da app atualizada para 2.0.1; várias ferramentas e scripts operacionais/validação adicionados.
  • Tests

    • Testes de redaction adicionados.

Review Change Stack

Copilot AI review requested due to automatic review settings May 13, 2026 20:29
@vercel
Copy link
Copy Markdown

vercel Bot commented May 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
zapp-web Ready Ready Preview, Comment May 13, 2026 9:43pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 13, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e43c5020-30ed-4976-90fa-910ac59d8df0

📥 Commits

Reviewing files that changed from the base of the PR and between 0ad35b5 and 213c567.

📒 Files selected for processing (1)
  • .github/workflows/deploy-vps.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/deploy-vps.yml

Walkthrough

PR 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.

Changes

Containerização, observabilidade, compliance e validação de build

Layer / File(s) Summary
Containerização multi-stage, nginx e deploy VPS
Dockerfile, docker-compose.yml, nginx.conf, .dockerignore, .github/workflows/deploy-vps.yml
Dockerfile: 3 estágios (deps/builder/runtime); nginx runtime com HEALTHCHECK; docker-compose: zapp-web com build-args, Traefik labels e healthcheck /healthz; .dockerignore atualizado; workflow deploy-vps builda/pusha imagem e faz deploy por SSH.
CI com typecheck strict, domain checks e security gates
.github/workflows/ci.yml, .github/workflows/security.yml, package.json (scripts)
ci.yml executa uma sequência Bun-driven de checks antes dos testes (typecheck, strict core, lint, domain/barrels, api validate, vps readiness, build, perf budget); security.yml adiciona dependency-review (fail on high) e refatora RLS audit para Bun/artifact; package.json amplia scripts e adiciona lint-staged.
OpenAPI 3.1.0 e validação automática
docs/api/openapi.json, scripts/validate-openapi.mjs
Adiciona especificação OpenAPI (health, public-api com x-api-key/x-idempotency-key, webhooks com HMAC) e validate-openapi.mjs que valida operationId único, summaries, responses, requestBody e mapeamento para supabase/functions/<name>/index.ts.
Redaction de PII/secrets e logger
src/lib/redaction.ts, src/lib/redaction.test.ts, src/lib/logger.ts, public/healthz, public/version.json, docs/security/auth-baseline.md, docs/observability/slos.md
Novo módulo redaction recursivo e circular-safe com regexs para tokens/emails/IDs; logger.ts usa redactLogArgs() para console e stringifyRedacted() para breadcrumbs; testes Vitest adicionados; health/version públicos e documentação de auth/SLOs incluídos.
public-api: idempotência e replay
supabase/functions/public-api/index.ts, supabase/migrations/*
public-api implementa getIdempotencyKey(), consulta por request_id e retorna replay em caso de duplicado; grava request_id no insert e trata conflitos com replay; migração adiciona índice parcial único em messages(request_id).
Performance budget, VPS readiness e RLS compliance
scripts/check-performance-budget.mjs, scripts/check-vps-readiness.mjs, scripts/verify_rls_compliance.ts, scripts/check-supabase-drift.mjs, scripts/manual-supabase-backup.sh, scripts/validate-supabase-backup-artifact.mjs, tsconfig.strict.core.json
Performance budget verifica bundles gzippados; VPS readiness checa artefatos Docker/Vite/config e ausência de imports proibidos; verify_rls_compliance varre migrations para RLS/policies; drift/backup scripts e validador de artifacts; tsconfig.strict.core.json define baseline estrita.
Vite build tuning e tsconfig
vite.config.ts, tsconfig.strict.core.json
vite.config.ts adiciona manualChunks (React/Supabase/Radix/TanStack/motion/charts/Sentry), target: es2020, sourcemap condicional e ajustes de build; tsconfig.strict.core.json inclui arquivos core.
Documentação: auditoria técnica, SLOs e changelog
docs/auditorias/*, docs/observability/slos.md, docs/security/auth-baseline.md, CHANGELOG.md, docs/operations/supabase-backup-and-drift-runbook.md
Adiciona auditoria técnica detalhada, runbook de backup/drift, SLOs/SLIs, auth baseline e changelog Unreleased (2026-05-13) documentando RLS gate, perf budget, redaction, OpenAPI e idempotência.

Sequence Diagram

sequenceDiagram
  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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed O título descreve com precisão os principais componentes do PR: CI gates, validação OpenAPI & RLS, deploy VPS, redação de logs e suporte a idempotência.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +79 to +94
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
);
}
Comment on lines +72 to +95
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
);
}
}
Comment thread src/lib/redaction.ts Outdated
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;
Comment thread .github/workflows/deploy-vps.yml Outdated
Comment on lines +75 to +91
-- 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));
Comment on lines +93 to +115
-- 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));
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread supabase/functions/public-api/index.ts Outdated
Comment on lines +72 to +77
if (idempotencyKey) {
const { data: previousMessage } = await supabase
.from('messages')
.select('id, contact_id, status, external_id')
.eq('request_id', idempotencyKey)
.maybeSingle();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread Dockerfile
COPY . .

ARG VITE_SUPABASE_URL
ARG VITE_SUPABASE_ANON_KEY
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 8555b50 and 1dc0c9b.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock, !**/*.lock
📒 Files selected for processing (26)
  • .dockerignore
  • .github/workflows/ci.yml
  • .github/workflows/deploy-vps.yml
  • .github/workflows/security.yml
  • CHANGELOG.md
  • Dockerfile
  • docker-compose.yml
  • docs/api/openapi.json
  • docs/auditorias/2026-05-13-auditoria-tecnica-exaustiva.md
  • docs/observability/slos.md
  • docs/security/auth-baseline.md
  • nginx.conf
  • package.json
  • public/healthz
  • public/version.json
  • scripts/check-performance-budget.mjs
  • scripts/check-vps-readiness.mjs
  • scripts/validate-openapi.mjs
  • scripts/verify_rls_compliance.ts
  • src/lib/logger.ts
  • src/lib/redaction.test.ts
  • src/lib/redaction.ts
  • supabase/functions/public-api/index.ts
  • supabase/migrations/20260513093000_rls_compliance_gap_closure.sql
  • tsconfig.strict.core.json
  • vite.config.ts

Comment thread .github/workflows/deploy-vps.yml
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Comment thread Dockerfile
Comment on lines +30 to +36
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;"]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

🧩 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."
fi

Repository: adm01-debug/zapp-web

Length of output: 393


🏁 Script executed:

# Procurar pelo nginx.conf
fd -name "nginx.conf" -type f

Repository: 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 f

Repository: 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:


🏁 Script executed:

fd nginx.conf

Repository: adm01-debug/zapp-web

Length of output: 127


🏁 Script executed:

fd docker-compose

Repository: adm01-debug/zapp-web

Length of output: 83


🏁 Script executed:

cat -n nginx.conf

Repository: adm01-debug/zapp-web

Length of output: 1528


🏁 Script executed:

cat -n docker-compose.yml

Repository: 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.

Comment thread docs/api/openapi.json
Comment on lines +155 to +160
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"429": {
"$ref": "#/components/responses/RateLimited"
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
"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.

Comment on lines +10 to +15
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),
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment thread src/lib/redaction.ts
Comment on lines +21 to +23
.replace(BRAZIL_PHONE_PATTERN, REDACTED)
.replace(WHATSAPP_JID_PATTERN, `${REDACTED}@$1`)
.replace(UUID_TOKEN_PATTERN, REDACTED);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment on lines +9 to +13
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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment thread supabase/functions/public-api/index.ts
Comment on lines +94 to +99
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));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

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.

@adm01-debug
Copy link
Copy Markdown
Owner Author

Atualizei o PR com o commit ad43043f para endereçar os 6 comentários pendentes:

  • Idempotência: replay agora reflete o status real da mensagem anterior e não força success: true em falhas.
  • Idempotência/race: adicionado índice único parcial em messages.request_id e tratamento de conflito 23505 para replay sem reprocessar.
  • Redaction: regex de telefone BR agora exige +55, evitando mascarar números operacionais soltos; teste adicionado.
  • Deploy VPS: ZAPP_WEB_DEPLOY_DIR agora é encaminhado pelo appleboy/ssh-action.
  • RLS: removidas leituras globais authenticated USING (true) para avatars/email_templates/contact_phones/salespeople e substituídas por escopos próprios/staff.

Validações locais executadas: api:validate, vps:check, security:rls, redaction.test.ts, typecheck:strict:core, build, perf:budget e git diff --check.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 1dc0c9b and ad43043.

📒 Files selected for processing (5)
  • .github/workflows/deploy-vps.yml
  • src/lib/redaction.test.ts
  • src/lib/redaction.ts
  • supabase/functions/public-api/index.ts
  • supabase/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

Comment on lines +19 to +21
permissions:
contents: read
packages: write
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cat -n .github/workflows/deploy-vps.yml

Repository: 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.

Suggested change
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.

Comment on lines +91 to +95
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: |
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 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:


🏁 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_DIR

Extraia 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).

Comment on lines +100 to +101
docker compose pull zapp-web || true
docker compose up -d zapp-web
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# Primeiro, vamos encontrar e examinar o arquivo de workflow
find . -name "deploy-vps.yml" -type f

Repository: 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 -120

Repository: 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:


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.

Suggested change
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.

Comment on lines +8 to +10
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 <> '';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
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).

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread nginx.conf
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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +8 to +10
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 <> '';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread nginx.conf

location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache" always;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@adm01-debug
Copy link
Copy Markdown
Owner Author

Atualizei novamente o PR com o commit 0ad35b5 para executar a parte repo-side do handoff de backup quebrado + drift Supabase:

  • Adicionado runbook operacional em docs/operations/supabase-backup-and-drift-runbook.md sem segredos.
  • Adicionado scripts/manual-supabase-backup.sh para backup lógico completo, sem --schema=public, com rejeição de PGHOST em IP, gunzip -t, SHA256, tamanho mínimo e cópia opcional.
  • Adicionado scripts/validate-supabase-backup-artifact.mjs para validar artifacts existentes.
  • Adicionado scripts/check-supabase-drift.mjs para comparar imagens declaradas no compose com docker service inspect.
  • Adicionados scripts ops:backup:validate e ops:drift:check no package.json.

Validações executadas localmente: bash -n, helps dos scripts, drift-check offline com fixture, npm run vps:check, npm run api:validate e git diff --check.

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.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/lib/redaction.ts
.replace(EMAIL_PATTERN, REDACTED)
.replace(CPF_CNPJ_PATTERN, REDACTED)
.replace(BRAZIL_PHONE_PATTERN, REDACTED)
.replace(WHATSAPP_JID_PATTERN, `${REDACTED}@$1`)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread docs/api/openapi.json
Comment on lines +425 to +428
"payload": {
"type": "object",
"additionalProperties": true
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread docs/api/openapi.json
Comment on lines +302 to +305
"hmacSignature": {
"type": "apiKey",
"in": "header",
"name": "x-signature"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
scripts/check-supabase-drift.mjs (1)

64-66: ⚡ Quick win

Tratamento de erro no --inspect-json pode 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

📥 Commits

Reviewing files that changed from the base of the PR and between ad43043 and 0ad35b5.

📒 Files selected for processing (5)
  • docs/operations/supabase-backup-and-drift-runbook.md
  • package.json
  • scripts/check-supabase-drift.mjs
  • scripts/manual-supabase-backup.sh
  • scripts/validate-supabase-backup-artifact.mjs
🚧 Files skipped from review as they are similar to previous changes (1)
  • package.json

Comment on lines +89 to +97
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 });
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment on lines +68 to +71
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested change
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>
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

Comment on lines +11 to +14
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),
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
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,
Fix with Cubic

- 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
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
run: bun run security:rls | tee rls-compliance-report.md
run: |
set -o pipefail
bun run security:rls | tee rls-compliance-report.md
Fix with Cubic

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);
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

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')) {
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
if (method !== 'get' && !operation.requestBody && !path.includes('webhook')) {
if (['post', 'put', 'patch'].includes(method) && !operation.requestBody && !path.includes('webhook')) {
Fix with Cubic

envs: ZAPP_WEB_IMAGE,ZAPP_WEB_HOST,ZAPP_WEB_DEPLOY_DIR
script: |
set -euo pipefail
cd "/opt/zapp-web"
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
cd "/opt/zapp-web"
cd "${ZAPP_WEB_DEPLOY_DIR:-/opt/zapp-web}"
Fix with Cubic

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
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
docker compose pull zapp-web || true
docker compose pull zapp-web
Fix with Cubic

Comment thread nginx.conf

location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache" always;
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

Comment thread src/lib/redaction.test.ts
'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]'
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

Comment thread docker-compose.yml
build:
context: .
args:
VITE_SUPABASE_URL: ${VITE_SUPABASE_URL:?required}
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

adm01-debug added a commit that referenced this pull request May 20, 2026
…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>
Copy link
Copy Markdown
Owner Author

Substituída por #148 — versão limpa rebaseada no main atual. Arquivos conflitantes (scripts/verify_rls_compliance.ts, vite.config.ts) foram omitidos pois já estavam no main; ci.yml e package.json foram mesclados manualmente para preservar mudanças anteriores de #143 e #147. Todas as mudanças únicas desta PR foram mergeadas via #148. ✅


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants