Skip to content

Exportação Verificada

O sistema de exportação verificada permite baixar relatórios — incluindo pacotes .sulfite assinados — exigindo que o solicitante confirme sua identidade via código OTP enviado por email.

┌──────────┐  POST /export/verify   ┌──────────────┐   email   ┌──────────┐
│  Cliente  │──────────────────────▶│  Server       │──────────▶│  Email   │
│           │◀── { status: sent }   │  (gera OTP)   │           │  (OTP)   │
│           │                       └──────────────┘           └──────────┘
│           │  POST /export/confirm
│           │  { email, reportId, code, format }
│           │──────────────────────▶┌──────────────┐
│           │◀── .sulfite assinado  │  Server       │
└──────────┘                        │  (verifica +  │
                                    │   assina +    │
                                    │   gera output)│
                                    └──────────────┘

Princípios:

  • O OTP é gerado com Random.secure()15 caracteres alfanuméricos por default
  • O código é armazenado como hash SHA-256 — nunca em plaintext
  • A comparação é timing-safe
  • Após verificação, o relatório é assinado com role: "export" e a identidade do usuário é registrada (VerifiedIdentity)
  • O OTP é consumido na verificação — não pode ser reutilizado

Endpoints

Política de exportação — GET /api/v1/export/policy

Autenticação: canGenerate (opcional — retorna dados genéricos em modo dev)

Retorna a configuração de export policy do servidor. O cliente usa isso para decidir se deve exibir o fluxo OTP.

Response 200:

json
{
  "protectedFormats": ["sulfite", "pdf"],
  "requireVerificationForAll": false,
  "otpExpirationSeconds": 300,
  "identityHints": ["@acme.com", "user@example.com"]
}
CampoDescrição
protectedFormatsFormatos que exigem verificação OTP
requireVerificationForAllSe true, todos os formatos precisam de OTP
otpExpirationSecondsTempo de vida do código em segundos
identityHintsDomínios/emails permitidos (para UI de dicas)

Solicitar OTP — POST /api/v1/export/verify

Autenticação: canGenerate (opcional em modo dev)

Gera e envia um código OTP para o email informado.

Body:

json
{
  "email": "user@acme.com",
  "reportId": "uuid-do-relatorio"
}

Response 200:

json
{
  "status": "sent",
  "expiresIn": 300
}

Erros:

StatusErroQuando
400invalid_emailEmail inválido
400validation_errorreportId ausente
403identity_not_allowedEmail/domínio não permitido por EXPORT_ALLOWED_IDENTITIES
404not_foundRelatório não encontrado
429rate_limitedCódigo já enviado há menos de 60s, ou limite por hora atingido (5/hora por email)
503otp_store_fullCapacidade do OTP store atingida
503otp_delivery_failedFalha no envio do email
503not_configuredOTP service não configurado (sem email provider)

Confirmar OTP e baixar — POST /api/v1/export/confirm

Autenticação: canGenerate (opcional em modo dev)

Verifica o código OTP e retorna o relatório exportado, assinado.

Body:

json
{
  "email": "user@acme.com",
  "reportId": "uuid-do-relatorio",
  "code": "ABC123XYZ456DEF",
  "format": "sulfite"
}
CampoObrigatórioDefaultDescrição
emailSimEmail que recebeu o OTP
reportIdSimID do relatório
codeSimCódigo OTP recebido
formatNãosulfitesulfite, pdf, html, csv, excel

Response 200: Bytes do relatório exportado.

Headers de resposta:

Content-Type: application/octet-stream  (sulfite) / application/pdf  (pdf) / ...
Content-Disposition: attachment; filename="nome-do-relatorio.sulfite"
X-Sulfite-Signed: true
X-Sulfite-Signed-By: user@acme.com
X-Sulfite-Signed-At: 2026-04-09T10:00:00.000Z

Erros:

StatusErroQuando
400invalid_emailEmail inválido
400validation_errorreportId ou code ausente
400invalid_codeCódigo incorreto
400otp_not_foundNenhum código pendente para este email/relatório
403identity_not_allowedEmail/domínio não permitido
404not_foundRelatório não encontrado
410expiredCódigo expirado
422invalid_reportRelatório sem definição válida
422package_rebuild_failedFalha ao preservar conteúdo do pacote .sulfite ao assinar
423max_attemptsTentativas esgotadas — solicitar novo código
503not_configuredOTP service ou signer não configurados

Configuração

Variáveis de ambiente

VariávelDefaultDescrição
EXPORT_ALLOWED_IDENTITIES— (todos)Emails ou domínios permitidos, separados por vírgula. Ex: user@example.com,acme.com
EXPORT_PROTECTED_FORMATSsulfite,pdfFormatos que exigem OTP
EXPORT_REQUIRE_ALLfalseSe true, todos os formatos exigem OTP
EXPORT_OTP_EXPIRATION300Expiração do OTP em segundos
EXPORT_OTP_MAX_ATTEMPTS3Tentativas antes de invalidar
EXPORT_OTP_LENGTH15Comprimento do código

Também é necessário configurar um email provider (Resend ou MailerSend). Veja Configuração → Email.

Requisitos

  • ENCRYPTION_KEY configurado (necessário para assinar o relatório após verificação)
  • Um email provider configurado (RESEND_API_TOKEN ou MAILERSEND_API_TOKEN)

Exemplo de configuração

bash
export ENCRYPTION_KEY="base64-encoded-32-byte-key"
export EXPORT_ALLOWED_IDENTITIES="acme.com,partner@example.com"
export EXPORT_PROTECTED_FORMATS="sulfite,pdf"
export EXPORT_OTP_EXPIRATION=300
export RESEND_API_TOKEN="re_..."
export RESEND_FROM_EMAIL="noreply@acme.com"

Fluxo completo

1. Cliente detecta a política

bash
GET /api/v1/export/policy
# → { "protectedFormats": ["sulfite", "pdf"], ... }

Se o formato desejado está em protectedFormats, exibir o fluxo OTP.

2. Solicitar OTP

bash
curl -X POST https://server.com/api/v1/export/verify \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "email": "user@acme.com", "reportId": "abc123" }'

# Response: { "status": "sent", "expiresIn": 300 }
# → Usuário recebe email com o código

3. Confirmar e baixar

bash
curl -X POST https://server.com/api/v1/export/confirm \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@acme.com",
    "reportId": "abc123",
    "code": "ABC123XYZ456DEF",
    "format": "sulfite"
  }' \
  -o relatorio.sulfite

O arquivo baixado é um pacote .sulfite assinado digitalmente com a identidade verificada do usuário.


Assinatura resultante

Após exportação verificada, o relatório carrega uma assinatura com role: "export":

json
{
  "_signatures": {
    "export": {
      "alg": "hmac-sha256",
      "key_id": "current",
      "value": "base64url...",
      "signed_at": "2026-04-09T10:00:00.000Z",
      "origin": "verified-export",
      "identity": {
        "email": "user@acme.com",
        "verified_at": "2026-04-09T10:00:00.000Z",
        "domain": "acme.com"
      }
    }
  }
}

Essa assinatura pode ser verificada via GET /api/v1/reports/:id/signatures. Veja Assinatura.


Segurança

ProteçãoDetalhe
OTP hashingSHA-256 — código nunca armazenado em plaintext
ComparaçãoTiming-safe (evita timing attacks)
Rate limit por IPHerdado do rate limiter global
Rate limit por email5 OTPs/hora por email
Rate limit de reenvio60s entre pedidos para o mesmo relatório
Tentativas máximas3 (configurável) — após esgotar, OTP invalidado
Identidades permitidasEXPORT_ALLOWED_IDENTITIES — bloqueia emails/domínios não autorizados
AuditoriaTodos os eventos logados (ver Segurança → Auditoria)

Sulfite do 🇧🇷 para o mundo © 2026 Rafael S. Pinheiro