openapi: "3.1.0"
info:
  title: LAM3 — Laniway Account Management v3
  version: "1.0.0"
  description: |
    API de gestão de contratos, clientes, e-mail, DNS e hosting da Laniway.

    **Autenticação:** Bearer token LIAM (exceto endpoints públicos como `/health`).

    **Parâmetro global `dry_run`:** qualquer endpoint aceita `dry_run: true` no corpo.
    O broker valida a requisição e desfaz (rollback) todas as escritas no banco ao
    final — útil para testar sem persistir. Não é declarado por endpoint. Endpoints
    com efeitos externos (ex.: emissão de certificado) tratam o `dry_run` adicionalmente,
    retornando um *preview* sem executar a operação externa.
servers:
  - url: https://api.lam3.laniway.com.br
    description: Produção
  - url: https://api.lam3.laniway.com
    description: Produção (Americas)
components:
  securitySchemes:
    liamBearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Bearer token LIAM
  schemas:
    DkimPurpose:
      type: string
      enum: [smtp, tep, marketing, other, external]
      description: |
        Finalidade do par de chaves DKIM.
        `external` indica chave gerada por provedor terceiro (ex.: SendGrid,
        Amazon SES, Mailgun) e importada via `/dkim/append/v1.0` — o LAM3
        não detém a chave privada nesse caso.
    DkimAlgorithm:
      type: string
      enum: [rsa-1024, rsa-2048, rsa-4096, ed25519]
      description: |
        Algoritmo criptográfico do par de chaves DKIM.
        - `rsa-1024` — RSA 1024-bit. Padrão legado; obrigatório para `smtp`.
          Chave pública ~188 chars em base64; cabe em um único registro TXT DNS.
        - `rsa-2048` — RSA 2048-bit. Requer DNS splitting (dois TXT ou
          registro único > 255 chars com suporte do bind). Usar quando
          o pipeline DNS estiver preparado.
        - `rsa-4096` — RSA 4096-bit. Idem acima, máxima segurança RSA.
        - `ed25519` — Ed25519 (RFC 8463). Chave pública de 44 chars em base64;
          elimina o problema de registros TXT grandes. Padrão para `tep`.
          Suporte amplo em MTAs modernos (Gmail, Microsoft 365, Yahoo,
          Postfix + OpenDKIM ≥ 2.11, rspamd).
    DkimStatusValue:
      type: string
      enum: [active, inactive, dns_pending, not_configured, pending_remove]
      description: Estado atual da configuração DKIM
    DkimRecord:
      type: object
      properties:
        dkim_id:
          type: string
          format: uuid
        dkim_selector:
          type: string
        dkim_purpose:
          $ref: "#/components/schemas/DkimPurpose"
        dkim_algorithm:
          $ref: "#/components/schemas/DkimAlgorithm"
        dkim_public_key:
          type: string
          description: "Registro TXT DNS completo, ex: v=DKIM1; h=sha256; k=rsa; p=... ou v=DKIM1; k=ed25519; p=..."
        dkim_status:
          $ref: "#/components/schemas/DkimStatusValue"
    DomainStatus:
      type: string
      enum: [activating, active, suspended, cancelled, unknown]
      description: |
        Estado do domínio.
        - `activating` — criado, ainda em processo de ativação
        - `active` — ativo e operacional
        - `suspended` — suspenso
        - `cancelled` — cancelado
        - `unknown` — valor não mapeado
    DomainAccountService:
      type: object
      description: Serviço vinculado ao domínio.
      properties:
        account_has_service_id:
          type: string
          format: uuid
          description: Identificador único do vínculo entre contrato e serviço
        account_id:
          type: string
          format: uuid
        service_id:
          type: string
          format: uuid
        service_type_id:
          type: integer
          example: 16
    DkimSummary:
      type: object
      description: Resumo de um seletor DKIM vinculado ao domínio
      properties:
        dkim_id:
          type: string
          format: uuid
        dkim_selector:
          type: string
          example: laniway2024
        dkim_purpose:
          $ref: "#/components/schemas/DkimPurpose"
        dkim_active:
          type: boolean
        dkim_dns_verified:
          type: boolean
    DomainGetResponse:
      type: object
      properties:
        domain_id:
          type: string
          format: uuid
        domain_name:
          type: string
          example: "dominio-exemplo.com.br"
        domain_status:
          $ref: "#/components/schemas/DomainStatus"
        customer_id:
          type: string
          format: uuid
        account_services:
          type: array
          items:
            $ref: "#/components/schemas/DomainAccountService"
          description: Serviços vinculados ao domínio. Pode ser vazio.
        domain_setup_date:
          type: string
          format: date-time
          nullable: true
        domain_status_date:
          type: string
          format: date-time
          nullable: true
        domain_require_tfa:
          type: boolean
        dkim:
          type: array
          items:
            $ref: "#/components/schemas/DkimSummary"
    Error:
      type: object
      properties:
        error:
          type: string
        code:
          type: integer

    ErrorResponse:
      type: object
      description: |
        Resposta de erro padrão LAM3.

        O campo `code` contém um inteiro numérico. Códigos **≥ 3 000 000** são
        específicos do LAM3 (projeto ID 3). Códigos menores são do framework BrokerNG
        (ex: 107 = parâmetro inválido, 112 = recurso não encontrado, 113 = sem permissão).

        **Catálogo de códigos LAM3 personalizados**

        | Código | HTTP | Módulo | Descrição |
        |--------|------|--------|-----------|
        | 3000010 | 406 | Email | Conta de e-mail sem vínculo de contrato ativo |
        | 3000020 | 403 | Email | Endereço de e-mail inacessível — apenas auto-consulta é permitida |
        | 3001001 | 500 | DKIM | Falha ao descriptografar a chave privada DKIM |
        | 3001002 | 406 | DKIM | Esta configuração DKIM já está ativa |
        | 3001003 | 406 | DKIM | Registro DNS TXT ainda não publicado para este DKIM |
        | 3001004 | 406 | DKIM | Esta configuração DKIM já está inativa |
        | 3001005 | 406 | DKIM | Esta configuração DKIM já está em processo de remoção |
        | 3001006 | 406 | DKIM | Este DKIM não está em estado pending_remove |
        | 3001007 | 406 | DKIM | Token de remoção DKIM expirado — status revertido para active |
        | 3001008 | 409 | DKIM | Já existe uma configuração DKIM com este selector para o domínio |
        | 3001009 | 409 | DKIM | Já existe uma configuração DKIM com este purpose e algorithm para o domínio |
        | 3001010 | 403 | DKIM | Este endpoint é restrito a contas de serviço (AppToken) |
        | 3001011 | 406 | DKIM | Valor de dkim_purpose inválido |
        | 3001012 | 406 | DKIM | Valor de dkim_algorithm inválido |
        | 3001013 | 406 | DKIM | purpose=smtp suporta apenas algoritmos RSA |
        | 3001014 | 406 | DKIM | Use o endpoint dkim/append para purpose=external |
        | 3001015 | 406 | DKIM | dkim_selector é obrigatório |
        | 3001016 | 406 | DKIM | dkim_public_key é obrigatória |
        | 3001017 | 500 | DKIM | Inconsistência de dados: operação DKIM requer domain válido com id |
        | 3001018 | 406 | DKIM | DKIM externo não possui chave privada gerenciada pelo LAM3 |
        | 3001019 | 406 | DKIM | Chave Ed25519 não possui formato PEM |
        | 3001020 | 401 | DKIM | Token de remoção DKIM inválido ou já utilizado |
        | 3001021 | 406 | DKIM | dkim/status requer dkim_id ou domain_name |
        | 3001022 | 406 | DKIM | dkim/status aceita apenas dkim_id ou domain_name, não ambos |
        | 3000021 | 404 | Email | Conta de e-mail não encontrada |
        | 3002001 | 500 | — | Erro interno de dados — contate o suporte |
        | 3002002 | 500 | — | Erro interno de dados — contate o suporte |
        | 3003001 | 500 | — | Erro interno de dados — contate o suporte |
        | 3003010 | 403 | Domínio | Acesso negado ao domínio |
        | 3003011 | 404 | Domínio | Domínio não encontrado |
        | 3004001 | 500 | — | Erro interno de dados — contate o suporte |
        | 3005001 | 404 | Evento | Evento não encontrado |
        | 3005002 | 409 | Evento | Possível race condition — repita a chamada sem avançar o cursor |

        > Códigos HTTP 500 (`3001001`, `3001017`, `3002001`, `3002002`, `3003001`, `3004001`)
        > indicam falha interna e nunca devem ocorrer em operação normal. Contate o suporte se encontrá-los.
      properties:
        error:
          type: string
          description: Mensagem de erro legível
        code:
          type: integer
          description: Código numérico de erro LAM3

    MailAccountStatus:
      type: string
      enum:
        - awaiting_provisioning
        - active
        - awaiting_suspension
        - suspended
        - awaiting_removal
        - removed
        - awaiting_reactivation
        - disabled
        - locked_quota
        - blocked_by_admin
        - compromised
        - server_failure
        - blocked_importing
        - blocked_import_error
        - provisioning
        - removing
        - reactivating
        - provisioning_failure
        - unknown
      description: Estado atual da conta de e-mail

    EmailAddressResponse:
      type: object
      description: Dados resolvidos de um endereço de e-mail.
      properties:
        email_address:
          type: string
          example: "usuario@dominio-exemplo.com.br"
        mail_account_id:
          type: integer
          description: Identificador numérico da conta de e-mail.
          example: 45257
        mail_account_status_id:
          type: integer
          example: 1
        mail_account_status:
          $ref: "#/components/schemas/MailAccountStatus"
        domain_id:
          type: string
          format: uuid
        domain_name:
          type: string
          example: "dominio-exemplo.com.br"
        account_id:
          type: string
          format: uuid
        customer_id:
          type: string
          format: uuid
        service_id:
          type: string
          format: uuid
        service_type_id:
          type: integer
          example: 16

    AccountStatus:
      type: string
      enum:
        - active
        - suspended
        - cancelled
        - unknown
      description: Estado do contrato (account).

    EmailContractResponse:
      type: object
      description: Dados do contrato vinculado a uma conta de e-mail.
      properties:
        mail_account_id:
          type: integer
          description: Identificador numérico da conta de e-mail.
          example: 12345
        email_address:
          type: string
          example: "usuario@dominio-exemplo.com.br"
        account_id:
          type: string
          format: uuid
          description: Identificador do contrato
        account_name:
          type: string
          example: "Contrato Email Empresarial"
          nullable: true
        account_alias:
          type: string
          example: "empresa-xpto"
          nullable: true
        account_status:
          $ref: "#/components/schemas/AccountStatus"
        plan_id:
          type: string
          format: uuid
        customer_id:
          type: string
          format: uuid
        service_id:
          type: string
          format: uuid

    MailAccountQuotaStatus:
      type: string
      enum:
        - ok
        - warning
        - critical
        - unknown
      description: |
        Nível de quota de armazenamento, decidido **centralmente** pelo LAM3
        (o consumidor não recalcula):
        - `ok` — uso abaixo de 80%
        - `warning` — uso >= 80%
        - `critical` — uso >= 99% **ou** espaço livre abaixo do piso
          (o maior entre 200KB e 2% da quota)
        - `unknown` — quota não definida na conta

    EmailQuotaResponse:
      type: object
      description: Quota de armazenamento (tamanho) de uma conta de e-mail.
      properties:
        mail_account_id:
          type: integer
          description: Identificador numérico da conta de e-mail.
          example: 45257
        email_address:
          type: string
          example: "usuario@dominio-exemplo.com.br"
        mail_account_quota_bytes:
          type: integer
          format: int64
          description: Quota total alocada, em bytes.
          example: 26214400000
        mail_account_quota_usage_bytes:
          type: integer
          format: int64
          description: Total usado, em bytes.
          example: 5242880
        mail_account_quota_free_bytes:
          type: integer
          format: int64
          description: Espaço livre (alocado − usado), em bytes; nunca negativo.
          example: 26209157120
        mail_account_quota_usage_percentile:
          type: number
          format: float
          description: Percentual de uso (0–100, 2 casas decimais).
          example: 0.02
        mail_account_quota_status:
          $ref: "#/components/schemas/MailAccountQuotaStatus"

    EmailRejectListEntryResponse:
      type: object
      description: Confirmação de um remetente registrado na lista de rejeição da caixa.
      properties:
        sender:
          type: string
          description: Endereço ou domínio do remetente que passou a ser rejeitado.
          example: "remetente-indesejado@dominio-exemplo.com.br"
        scope:
          type: string
          enum:
            - address
            - domain
          description: |
            Abrangência do bloqueio:
            - `address` — bloqueia apenas o endereço de e-mail informado.
            - `domain` — bloqueia o domínio inteiro do remetente.
          example: "address"
        recipient:
          type: string
          description: A caixa de e-mail à qual a regra se aplica (o próprio dono).
          example: "usuario@dominio-exemplo.com.br"
        type:
          type: string
          enum:
            - reject
          description: Tipo da regra. Este endpoint cria sempre regras de rejeição.
          example: "reject"
        created_at:
          type: string
          format: date-time
          description: Data/hora do registro da regra (ISO 8601).
          example: "2026-06-30T10:15:00"
        already_existed:
          type: boolean
          description: |
            `true` se a regra já existia (operação idempotente — nenhuma
            duplicata é criada).
          example: false

    EmailRejectListItem:
      type: object
      description: Uma regra da lista de rejeição da caixa.
      properties:
        sender:
          type: string
          example: "remetente-indesejado@dominio-exemplo.com.br"
        scope:
          type: string
          enum:
            - address
            - domain
          example: "address"
        recipient:
          type: string
          description: A caixa à qual a regra se aplica.
          example: "usuario@dominio-exemplo.com.br"
        type:
          type: string
          enum:
            - reject
          example: "reject"
        created_at:
          type: string
          format: date-time
          example: "2026-06-30T10:15:00"

    EmailRejectListResponse:
      type: object
      description: Lista de rejeição da caixa do usuário autenticado.
      properties:
        recipient:
          type: string
          description: A caixa cujas regras são listadas (o próprio dono).
          example: "usuario@dominio-exemplo.com.br"
        count:
          type: integer
          description: Quantidade de regras retornadas.
          example: 2
        entries:
          type: array
          items:
            $ref: "#/components/schemas/EmailRejectListItem"

    EmailRejectListRemoveResponse:
      type: object
      description: Resultado da remoção de um remetente da lista de rejeição.
      properties:
        sender:
          type: string
          example: "remetente-indesejado@dominio-exemplo.com.br"
        scope:
          type: string
          enum:
            - address
            - domain
          example: "address"
        recipient:
          type: string
          example: "usuario@dominio-exemplo.com.br"
        type:
          type: string
          enum:
            - reject
          example: "reject"
        created_at:
          type: string
          format: date-time
          nullable: true
          description: |
            Momento do registro original da regra removida; `null` se a regra
            não existia (remoção idempotente).
          example: "2026-06-30T10:15:00"
        removed:
          type: boolean
          description: |
            `true` se uma regra foi removida; `false` se não havia regra
            correspondente (operação idempotente).
          example: true

    EventRecord:
      type: object
      description: Registro de evento de negócio.
      properties:
        event_id:
          type: integer
          description: Identificador único do evento.
          example: 1359001
        event_type_id:
          type: string
          description: Tipo do evento (constante EV_*).
          example: "EV_EMAIL_ACCOUNT_ADD"
        event_date:
          type: string
          format: date-time
          description: Data/hora do registro do evento (ISO 8601).
          example: "2026-05-08T14:00:00"
        event_archived:
          type: boolean
          description: true se o evento foi arquivado.
          example: false
        event_data:
          type: object
          description: Payload do evento. Estrutura varia por `event_type_id`.
          additionalProperties: true
security:
  - liamBearerAuth: []
paths:
  /health:
    get:
      summary: Health check
      operationId: health_get
      description: Verifica se o serviço está operacional. Não requer autenticação.
      security: []
      tags:
        - Sistema
      responses:
        "200":
          description: Serviço operacional
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      status:
                        type: string
                        example: "ok"
              example:
                data:
                  status: "ok"

  /dkim/add/v1.0:
    post:
      summary: Criar configuração DKIM
      operationId: dkim_add
      description: |
        Gera um par de chaves para o domínio informado, insere o registro TXT
        na zona DNS gerenciada (se existir) e persiste a configuração criptografada.

        **Algoritmos suportados** (`dkim_algorithm`):
        - `rsa-1024` — RSA 1024-bit. Padrão para `smtp`.
          `smtp` aceita apenas RSA (`rsa-1024`, `rsa-2048` ou `rsa-4096`).
        - `ed25519` — Ed25519 (RFC 8463). Padrão para `tep`. Chave pública de ~44 chars;
          elimina o problema de registros TXT > 255 chars. Suporte amplo em MTAs modernos
          (Gmail, Microsoft 365, Yahoo, Postfix + OpenDKIM ≥ 2.11, rspamd).

        Para purposes únicos (`smtp`, `tep`) é permitido **um seletor por algoritmo**,
        ou seja, até dois simultâneos por purpose (um RSA + um Ed25519).

        Permissão: `lam3.email.dkim.add`.
      tags:
        - DKIM
      security:
        - liamBearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain_name]
              properties:
                domain_name:
                  type: string
                  example: "cliente.com.br"
                dkim_purpose:
                  $ref: "#/components/schemas/DkimPurpose"
                  default: other
                dkim_algorithm:
                  $ref: "#/components/schemas/DkimAlgorithm"
                  description: |
                    Algoritmo do par de chaves. Padrão por purpose:
                    smtp=rsa-1024 (fixo), tep=ed25519, demais=rsa-1024.
                    smtp aceita apenas rsa-1024, rsa-2048 ou rsa-4096.
      responses:
        "200":
          description: Configuração DKIM criada
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    allOf:
                      - $ref: "#/components/schemas/DkimRecord"
        "406":
          description: |
            Validação de negócio. Códigos LAM3 possíveis:
            - `3001011` — dkim_purpose inválido
            - `3001012` — dkim_algorithm inválido
            - `3001013` — purpose=smtp suporta apenas algoritmos RSA
            - `3001014` — use dkim/append para purpose=external
        "409":
          description: |
            Conflito de dados. Códigos LAM3 possíveis:
            - `3001008` — já existe DKIM com este selector para o domínio
            - `3001009` — já existe DKIM com este purpose e algorithm para o domínio
        "401":
          description: Token inválido ou ausente
        "403":
          description: Sem permissão LIAM (`lam3.email.dkim.add`)
        "404":
          description: Domínio não encontrado (code `3003011`)

  /dkim/append/v1.0:
    post:
      summary: Importar configuração DKIM externa
      operationId: dkim_append
      description: |
        Registra no LAM3 uma chave DKIM gerada por um provedor terceiro
        (ex.: SendGrid, Amazon SES, Mailgun). O cliente fornece `dkim_selector`
        e `dkim_public_key`; a chave privada permanece no provedor e o LAM3
        gerencia apenas a publicação do registro DNS TXT (na zona ativa do
        domínio, se gerenciada), a verificação de publicação e a visibilidade
        no painel.
        Resulta em um registro com `dkim_purpose=external`. Esse registro **não**
        é retornável via `/dkim/get_pk/v1.0` (não há chave privada).
        Permissão: `lam3.email.dkim.append`.
      tags:
        - DKIM
      security:
        - liamBearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain_name, dkim_selector, dkim_public_key]
              properties:
                domain_name:
                  type: string
                  example: "cliente.com.br"
                dkim_selector:
                  type: string
                  example: "s1"
                dkim_public_key:
                  type: string
                  example: "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GN..."
      responses:
        "200":
          description: Configuração DKIM externa importada
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    allOf:
                      - $ref: "#/components/schemas/DkimRecord"
        "406":
          description: |
            Validação de negócio. Códigos LAM3 possíveis:
            - `3001015` — dkim_selector é obrigatório
            - `3001016` — dkim_public_key é obrigatória
        "409":
          description: Seletor já existente para o domínio (code `3001008`)
        "401":
          description: Token inválido ou ausente
        "403":
          description: Sem permissão LIAM (`lam3.email.dkim.append`)
        "404":
          description: Domínio não encontrado (code `3003011`)

  /dkim/activate/v1.0:
    post:
      summary: Ativar assinatura DKIM
      operationId: dkim_activate
      description: |
        Verifica a publicação DNS do registro TXT e ativa a assinatura.
        Permissão: `lam3.email.dkim.activate`.
      tags:
        - DKIM
      security:
        - liamBearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [dkim_id]
              properties:
                dkim_id:
                  type: string
                  format: uuid
      responses:
        "200":
          description: Assinatura DKIM ativada
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      dkim_id:
                        type: string
                        format: uuid
                      dkim_selector:
                        type: string
                      dkim_status:
                        $ref: "#/components/schemas/DkimStatusValue"
        "406":
          description: |
            Validação de negócio. Códigos LAM3 possíveis:
            - `3001002` — esta configuração DKIM já está ativa
            - `3001003` — registro DNS TXT ainda não publicado
        "401":
          description: Token inválido ou ausente
        "403":
          description: Sem permissão LIAM (`lam3.email.dkim.activate`)
        "404":
          description: Não encontrado

  /dkim/deactivate/v1.0:
    post:
      summary: Desativar assinatura DKIM
      operationId: dkim_deactivate
      description: |
        Desativa a assinatura DKIM sem remover o registro DNS.
        Permissão: `lam3.email.dkim.deactivate`.
      tags:
        - DKIM
      security:
        - liamBearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [dkim_id]
              properties:
                dkim_id:
                  type: string
                  format: uuid
      responses:
        "200":
          description: Assinatura DKIM desativada
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      dkim_id:
                        type: string
                        format: uuid
                      dkim_selector:
                        type: string
                      dkim_status:
                        $ref: "#/components/schemas/DkimStatusValue"
        "406":
          description: Esta configuração DKIM já está inativa (code `3001004`)
        "401":
          description: Token inválido ou ausente
        "403":
          description: Sem permissão LIAM (`lam3.email.dkim.deactivate`)
        "404":
          description: Não encontrado

  /dkim/remove/v1.0:
    post:
      summary: Remover configuração DKIM (síncrono)
      operationId: dkim_remove
      description: |
        Remove a configuração DKIM e o registro DNS TXT associado de forma síncrona.
        Para remoções que fazem parte de um fluxo que exige confirmação explícita antes
        da exclusão, utilize o protocolo Saga: `/dkim/remove/prepare/v1.0`,
        `/dkim/remove/commit/v1.0`, `/dkim/remove/abort/v1.0`.
        Permissão: `lam3.email.dkim.remove`.
      tags:
        - DKIM
      security:
        - liamBearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [dkim_id]
              properties:
                dkim_id:
                  type: string
                  format: uuid
      responses:
        "200":
          description: Configuração DKIM removida
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      dkim_id:
                        type: string
                        format: uuid
                      dkim_selector:
                        type: string
        "401":
          description: Token inválido ou ausente
        "403":
          description: Sem permissão
        "404":
          description: Não encontrado

  /dkim/remove/prepare/v1.0:
    post:
      summary: Iniciar remoção DKIM (Saga prepare)
      operationId: dkim_remove_prepare
      description: |
        Inicia o protocolo Saga para remoção de DKIM.

        Retorna um `remove_token` válido por 10 minutos. Após obter o token:
        - Chamar `commit` para confirmar a remoção.
        - Chamar `abort` para cancelar e restaurar o estado anterior.

        Permissão: `lam3.email.dkim.remove.prepare`.
      tags:
        - DKIM
      security:
        - liamBearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [dkim_id]
              properties:
                dkim_id:
                  type: string
                  format: uuid
      responses:
        "200":
          description: Remoção preparada — token emitido
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      dkim_id:
                        type: string
                        format: uuid
                      remove_token:
                        type: string
                        format: uuid
                        description: Token de remoção a ser usado em commit ou abort
                      expires_in_minutes:
                        type: integer
                        example: 10
        "401":
          description: Token inválido ou ausente
        "403":
          description: Sem permissão LIAM (`lam3.email.dkim.remove.prepare`)
        "404":
          description: Não encontrado
        "406":
          description: DKIM já está em processo de remoção (code `3001005`)

  /dkim/remove/commit/v1.0:
    post:
      summary: Confirmar remoção DKIM (Saga commit)
      operationId: dkim_remove_commit
      description: |
        Confirma a remoção do DKIM após a operação que originou o `prepare` ser concluída
        com sucesso. Remove o registro DNS TXT e exclui o registro DKIM.

        Permissão: `lam3.email.dkim.remove.commit`.
      tags:
        - DKIM
      security:
        - liamBearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [remove_token]
              properties:
                remove_token:
                  type: string
                  format: uuid
      responses:
        "200":
          description: Remoção DKIM confirmada
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      dkim_id:
                        type: string
                        format: uuid
                      dkim_selector:
                        type: string
        "401":
          description: Token de remoção inválido ou já utilizado (code `3001020`)
        "403":
          description: Sem permissão LIAM (`lam3.email.dkim.remove.commit`)
        "404":
          description: Não encontrado
        "406":
          description: |
            Validação de negócio. Códigos LAM3 possíveis:
            - `3001006` — DKIM não está em estado pending_remove
            - `3001007` — token expirado (TTL 10 min) — DKIM revertido para active

  /dkim/remove/abort/v1.0:
    post:
      summary: Cancelar remoção DKIM (Saga abort)
      operationId: dkim_remove_abort
      description: |
        Cancela a remoção pendente e restaura o DKIM para o estado `active`.
        Deve ser chamado quando a operação que originou o `prepare` não for concluída
        com sucesso.

        Permissão: `lam3.email.dkim.remove.abort`.
      tags:
        - DKIM
      security:
        - liamBearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [remove_token]
              properties:
                remove_token:
                  type: string
                  format: uuid
      responses:
        "200":
          description: Remoção DKIM cancelada — DKIM restaurado
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      dkim_id:
                        type: string
                        format: uuid
                      dkim_selector:
                        type: string
                      dkim_status:
                        type: string
                        example: active
        "401":
          description: Token inválido ou ausente
        "403":
          description: Sem permissão
        "404":
          description: Não encontrado

  /dkim/status/v1.0:
    post:
      summary: Status de configuração DKIM
      operationId: dkim_status
      description: |
        Retorna o status de um seletor específico (`dkim_id`) ou de todos os seletores
        de um domínio (`domain_name`). Exatamente um dos dois parâmetros deve ser fornecido.

        Cada registro retornado inclui `fqdn` e `dns_published` indicando se o
        registro TXT já está visível no NS autoritativo (consulta direta, sem cache).
        Permissão: `lam3.email.dkim.status`.
      tags:
        - DKIM
      security:
        - liamBearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                dkim_id:
                  type: string
                  format: uuid
                domain_name:
                  type: string
                  example: "cliente.com.br"
      responses:
        "200":
          description: Status DKIM
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      domain_name:
                        type: string
                      dkim_records:
                        type: array
                        description: >
                          Lista de seletores DKIM do domínio. Vazia se não houver nenhum configurado.
                          Quando chamado com `dkim_id`, contém exatamente um elemento.
                        items:
                          type: object
                          properties:
                            dkim_id:
                              type: string
                              format: uuid
                            dkim_selector:
                              type: string
                            dkim_purpose:
                              $ref: "#/components/schemas/DkimPurpose"
                            dkim_algorithm:
                              $ref: "#/components/schemas/DkimAlgorithm"
                            dkim_public_key:
                              type: string
                            dkim_status:
                              $ref: "#/components/schemas/DkimStatusValue"
                            fqdn:
                              type: string
                              example: "a1b2c3._domainkey.cliente.com.br"
                            dns_published:
                              type: boolean
                              description: true se o TXT está visível no NS autoritativo
        "406":
          description: |
            Parâmetros inválidos. Códigos LAM3 possíveis:
            - `3001021` — nenhum parâmetro informado (requer dkim_id ou domain_name)
            - `3001022` — ambos os parâmetros informados (informe apenas um)
        "401":
          description: Token inválido ou ausente
        "403":
          description: Sem permissão LIAM (`lam3.email.dkim.status`)
        "404":
          description: Não encontrado

  /dkim/list/v1.0:
    post:
      summary: Listar configurações DKIM do domínio
      operationId: dkim_list
      description: |
        Lista todos os seletores DKIM vinculados a um domínio, com metadados básicos.
        Permissão: `lam3.email.dkim.list`.
      tags:
        - DKIM
      security:
        - liamBearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain_name]
              properties:
                domain_name:
                  type: string
                  example: "cliente.com.br"
      responses:
        "200":
          description: Lista de registros DKIM
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      domain_name:
                        type: string
                      dkim_records:
                        type: array
                        items:
                          type: object
                          properties:
                            dkim_id:
                              type: string
                              format: uuid
                            dkim_selector:
                              type: string
                            dkim_purpose:
                              $ref: "#/components/schemas/DkimPurpose"
                            dkim_algorithm:
                              $ref: "#/components/schemas/DkimAlgorithm"
                            dkim_active:
                              type: boolean
                            dkim_dns_verified:
                              type: boolean
                            dkim_status:
                              $ref: "#/components/schemas/DkimStatusValue"
        "401":
          description: Token inválido ou ausente
        "403":
          description: Sem permissão
        "404":
          description: Não encontrado

  /dkim/get_pk/v1.0:
    post:
      summary: Obter chave privada DKIM (Base64)
      operationId: dkim_get_pk
      description: |
        Retorna a **chave privada DKIM** do seletor, no formato adequado ao algoritmo:
        - **RSA**: PEM codificado em Base64 (comportamento original).
        - **Ed25519**: seed de 32 bytes em Base64 puro (44 chars) — formato esperado
          pelo OpenDKIM e demais MTAs para chaves Ed25519.

        > **Restrito a contas de serviço (AppToken).** Tokens de usuário humano são recusados.

        Cada chamada é registrada em log de auditoria.
        Permissão: `lam3.email.dkim.get_pk`.
        Rate limit: 60 req/min por IP.
      tags:
        - DKIM
      security:
        - liamBearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [dkim_id]
              properties:
                dkim_id:
                  type: string
                  format: uuid
      responses:
        "200":
          description: Dados DKIM completos
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      domain_name:
                        type: string
                      dkim_id:
                        type: string
                        format: uuid
                      dkim_selector:
                        type: string
                      dkim_algorithm:
                        $ref: "#/components/schemas/DkimAlgorithm"
                      dkim_public_key:
                        type: string
                      dkim_private_key_b64:
                        type: string
                        nullable: true
                        description: |
                          RSA: PEM codificado em Base64.
                          Ed25519: seed de 32 bytes em Base64 puro (44 chars).
                          `null` para purpose=external (chave não gerenciada pelo LAM3).
                      dkim_status:
                        $ref: "#/components/schemas/DkimStatusValue"
                      dkim_purpose:
                        $ref: "#/components/schemas/DkimPurpose"
        "401":
          description: Token inválido ou ausente
        "403":
          description: |
            Acesso negado. Códigos LAM3 possíveis:
            - `3001010` — endpoint restrito a contas de serviço (AppToken)
        "404":
          description: Não encontrado
        "406":
          description: |
            Validação de negócio. Códigos LAM3 possíveis:
            - `3001018` — DKIM externo não possui chave privada gerenciada pelo LAM3
            - `3001019` — chave Ed25519 não possui formato PEM
        "429":
          description: Rate limit atingido
        "500":
          description: Falha ao descriptografar a chave privada (code `3001001`)

  /sync/lam2/v1.0:
    post:
      summary: Orquestrador de sincronização LAM2 ↔ LAM3
      operationId: sync_lam2
      description: |
        Executa o orquestrador de sincronização agendada do LAM3.

        Rotinas executadas a cada ciclo:
        - Importação incremental de configurações DKIM pré-existentes (idempotente)
        - Sincronização de contas de e-mail com o sistema de identidades

        O orquestrador mantém ponteiros por rotina para processamento contínuo.
        Se a sincronização estiver desabilitada neste servidor, retorna `{"status": "disabled"}`.

        Permissão: `lam3.sync.lam2.run`.
      tags:
        - Sistema
      security:
        - liamBearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
      responses:
        "200":
          description: Resultado da sincronização
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      status:
                        type: string
                        enum: [ok, sync_already_running, disabled, error]
                        example: ok
                      orchestrator:
                        type: object
                        description: Resumo das rotinas executadas no ciclo atual.
              examples:
                concluido:
                  summary: Ciclo do orquestrador executado com sucesso
                  value:
                    data:
                      status: ok
                desabilitado:
                  summary: Sync desabilitado neste servidor
                  value:
                    data:
                      status: disabled
        "401":
          description: Token inválido ou ausente
        "403":
          description: Sem permissão (`lam3.sync.lam2.run`)

  /domain/get/v1.0:
    post:
      summary: Consultar domínio pelo nome
      operationId: domain_get
      description: |
        Retorna os dados completos de um domínio identificado pelo `domain_name`,
        incluindo status, `customer_id`, lista de serviços vinculados
        e seletores DKIM configurados.

        **Caso de uso principal:** o TEP extrai `domain_name` do endereço de e-mail
        recebido e chama este endpoint para identificar o `customer_id` e os serviços
        associados ao domínio.

        Permissão: `lam3.domain.read`.
      tags:
        - Domain
      security:
        - liamBearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain_name]
              properties:
                domain_name:
                  type: string
                  maxLength: 253
                  example: "dominio-exemplo.com.br"
      responses:
        "200":
          description: Dados do domínio
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/DomainGetResponse"
              examples:
                ativo_com_servicos:
                  summary: Domínio ativo com serviços vinculados
                  value:
                    data:
                      domain_id: "550e8400-e29b-41d4-a716-446655440000"
                      domain_name: "dominio-exemplo.com.br"
                      domain_status: "active"
                      customer_id: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
                      account_services:
                        - account_has_service_id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                          account_id: "a87ff679-a2f3-451d-8329-08b4b13e4d24"
                          service_id: "c4ca4238-a0b9-3382-8dcc-509a6f75849b"
                          service_type_id: 16
                      domain_setup_date: "2020-01-15T10:30:00"
                      domain_status_date: "2024-06-01T12:00:00"
                      domain_require_tfa: false
                      dkim:
                        - dkim_id: "f3b4c2d1-e5a6-47b8-c9d0-1e2f3a4b5c6d"
                          dkim_selector: "laniway2024"
                          dkim_purpose: "tep"
                          dkim_active: true
                          dkim_dns_verified: true
                ativo_sem_servicos:
                  summary: Domínio ativo sem serviços vinculados
                  value:
                    data:
                      domain_id: "550e8400-e29b-41d4-a716-446655440001"
                      domain_name: "legado.com.br"
                      domain_status: "active"
                      customer_id: "6ba7b810-9dad-11d1-80b4-00c04fd430c9"
                      account_services: []
                      domain_setup_date: "2018-03-10T08:00:00"
                      domain_status_date: null
                      domain_require_tfa: false
                      dkim: []
        "401":
          description: Token inválido ou ausente
        "403":
          description: Sem permissão LIAM (`lam3.domain.read`)
        "404":
          description: Domínio não encontrado (code `3003011`)

  /domain/list/v1.0:
    post:
      summary: Listar domínios acessíveis ao token
      operationId: domain_list
      description: |
        Retorna todos os domínios acessíveis ao token autenticado.

        Os tenants (customers) acessíveis ao token são consultados via memberships
        ativas. Se `customer_id` for informado, valida-se que pertence aos tenants
        acessíveis antes de filtrar — caso contrário retorna 403 sem revelar se o
        recurso existe (anti-enumeração).

        Permissão: `lam3.domain.list`.
      tags:
        - Domain
      security:
        - liamBearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                customer_id:
                  type: string
                  format: uuid
                  description: Filtra pelos domínios de um customer específico. Opcional.
                domain_status_filter:
                  type: string
                  enum: [activating, active, suspended, cancelled]
                  description: Filtra domínios pelo status. Opcional.
      responses:
        "200":
          description: Lista de domínios
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      domains:
                        type: array
                        items:
                          $ref: "#/components/schemas/DomainGetResponse"
              example:
                data:
                  domains:
                    - domain_id: "550e8400-e29b-41d4-a716-446655440000"
                      domain_name: "dominio-exemplo.com.br"
                      domain_status: "active"
                      customer_id: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
                      account_services:
                        - account_has_service_id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                          account_id: "a87ff679-a2f3-451d-8329-08b4b13e4d24"
                          service_id: "c4ca4238-a0b9-3382-8dcc-509a6f75849b"
                          service_type_id: 16
                      domain_setup_date: "2020-01-15T10:30:00"
                      domain_status_date: "2024-06-01T12:00:00"
                      domain_require_tfa: false
                      dkim:
                        - dkim_id: "f3b4c2d1-e5a6-47b8-c9d0-1e2f3a4b5c6d"
                          dkim_selector: "laniway2024"
                          dkim_purpose: "tep"
                          dkim_active: true
                          dkim_dns_verified: true
        "401":
          description: Token inválido ou ausente
        "403":
          description: |
            Acesso negado. Códigos LAM3 possíveis:
            - `3003010` — sem permissão ou `customer_id` não acessível ao token

  /email/address/get/v1.0:
    post:
      summary: Resolver endereço de e-mail (auto-consulta)
      operationId: email_address_get
      description: |
        Resolve um endereço de e-mail e retorna os dados do `mail_account`,
        incluindo o vínculo com contrato (`account_has_service_id`),
        domínio, account, customer e service_type.

        **Acesso por auto-consulta apenas:** a identity autenticada só pode
        consultar o próprio endereço de e-mail (`identity.email == email_address`).
        Para proteger contra enumeração, endereços não acessíveis e contas
        inexistentes retornam o mesmo código `3000020` (HTTP 403).

        **Sem RBAC:** não requer permissão específica além do token LIAM válido.
        A autorização é verificada dentro do handler.
      tags:
        - Email
      security:
        - liamBearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email_address]
              properties:
                email_address:
                  type: string
                  maxLength: 320
                  example: "usuario@dominio-exemplo.com.br"
      responses:
        "200":
          description: Endereço de e-mail resolvido
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/EmailAddressResponse"
              example:
                data:
                  email_address: "usuario@dominio-exemplo.com.br"
                  mail_account_id: 45257
                  mail_account_status_id: 1
                  mail_account_status: "active"
                  domain_id: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
                  domain_name: "dominio-exemplo.com.br"
                  account_id: "a87ff679-a2f3-451d-8329-08b4b13e4d24"
                  customer_id: "c4ca4238-a0b9-3382-8dcc-509a6f75849b"
                  service_id: "f3b4c2d1-e5a6-47b8-c9d0-1e2f3a4b5c6d"
                  service_type_id: 16
        "401":
          description: Token inválido ou ausente
        "403":
          description: |
            Acesso negado — anti-enumeração. Código LAM3:
            - `3000020` — endereço não é auto-consulta ou conta não existe
        "406":
          description: Conta sem vínculo de contrato ativo (code `3000010`)

  /email/contract/get/v1.0:
    post:
      summary: Resolver contrato de uma conta de e-mail
      operationId: email_contract_get
      description: |
        Retorna os dados do contrato (`account`) vinculado a uma conta de e-mail.

        Requer a permissão `lam3.email.contract.get` — pode ser concedida a
        qualquer identity LIAM (usuário humano ou app token).

        **Identificação:** forneça exatamente **um** dos parâmetros:
        - `email_address` — endereço completo `login@dominio`
        - `mail_account_id` — identificador numérico da conta de e-mail

        Fornecer nenhum ou ambos retorna código `107`.
      tags:
        - Email
      security:
        - liamBearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email_address:
                  type: string
                  maxLength: 320
                  example: "usuario@dominio-exemplo.com.br"
                mail_account_id:
                  type: integer
                  example: 12345
      responses:
        "200":
          description: Contrato resolvido
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/EmailContractResponse"
              example:
                data:
                  mail_account_id: 12345
                  email_address: "usuario@dominio-exemplo.com.br"
                  account_id: "a87ff679-a2f3-451d-8329-08b4b13e4d24"
                  account_name: "Contrato Email Empresarial"
                  account_alias: "empresa-xpto"
                  account_status: "active"
                  plan_id: "d3d9446802a44259755d38e6d163e820"
                  customer_id: "c4ca4238-a0b9-3382-8dcc-509a6f75849b"
                  service_id: "f3b4c2d1-e5a6-47b8-c9d0-1e2f3a4b5c6d"
        "400":
          description: Parâmetro inválido — nenhum ou ambos os identificadores fornecidos (code `107`)
        "401":
          description: Token inválido ou ausente
        "403":
          description: Identity sem permissão `lam3.email.contract.get` (code `113`)
        "404":
          description: Conta de e-mail não encontrada (code `3000021`)
        "406":
          description: Conta sem vínculo de contrato (code `3000010`)

  /email/quota/get/v1.0:
    post:
      summary: Consultar quota de armazenamento de uma conta de e-mail
      operationId: email_quota_get
      description: |
        Retorna a quota de armazenamento (tamanho) de uma conta de e-mail —
        total alocado, usado, livre, percentual e o **nível**
        (`mail_account_quota_status`). O endpoint responde apenas a situação
        **atual**: a atualização do "usado" é feita por outro canal (os
        servidores de armazenamento reportam periodicamente).

        O nível (`ok`/`warning`/`critical`/`unknown`) é decidido **centralmente**
        pelo LAM3, para que o consumidor (webmail) não precise replicar a regra
        de severidade. Todos os tamanhos são em **bytes**.

        Requer a permissão `lam3.email.quota.get`. Acesso por **auto-consulta**
        (o dono da caixa, cuja identity LIAM é o próprio endereço) ou por
        **staff/admin do tenant** (`role-tenant-master`/`role-tenant-tech`).

        **Identificação:** forneça exatamente **um** dos parâmetros:
        - `email_address` — endereço completo `login@dominio`
        - `mail_account_id` — identificador numérico da conta de e-mail

        Fornecer nenhum ou ambos retorna código `107`.
      tags:
        - Email
      security:
        - liamBearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email_address:
                  type: string
                  maxLength: 320
                  example: "usuario@dominio-exemplo.com.br"
                mail_account_id:
                  type: integer
                  example: 45257
      responses:
        "200":
          description: Quota de armazenamento resolvida
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/EmailQuotaResponse"
              example:
                data:
                  mail_account_id: 45257
                  email_address: "usuario@dominio-exemplo.com.br"
                  mail_account_quota_bytes: 26214400000
                  mail_account_quota_usage_bytes: 5242880
                  mail_account_quota_free_bytes: 26209157120
                  mail_account_quota_usage_percentile: 0.02
                  mail_account_quota_status: "ok"
        "400":
          description: Parâmetro inválido — nenhum ou ambos os identificadores fornecidos (code `107`)
        "401":
          description: Token inválido ou ausente
        "403":
          description: Identity sem permissão `lam3.email.quota.get` (code `113`)
        "404":
          description: Conta de e-mail não encontrada (code `3000021`)

  /email/reject_list/add/v1.0:
    post:
      summary: Adicionar remetente à lista de rejeição da própria caixa
      operationId: email_reject_list_add
      description: |
        Registra um remetente (endereço de e-mail ou domínio) na **lista de
        rejeição** da caixa do usuário autenticado. Mensagens desse remetente
        passam a ser recusadas na entrega para essa caixa.

        **Auto-serviço:** a regra é sempre aplicada à caixa cujo dono é a
        identity autenticada (`identity.email`). Não há como agir sobre a caixa
        de terceiros — o destinatário não é um parâmetro. Apenas identidades que
        correspondem a uma conta de e-mail gerenciada podem usar o endpoint.

        **Abrangência (`scope`):**
        - `address` (padrão) — bloqueia apenas o endereço de e-mail informado.
        - `domain` — bloqueia o domínio inteiro do remetente. Subdomínios são
          reduzidos ao domínio registrável (ex.: `mail.spam.com` → `spam.com`);
          o `sender` retornado reflete o valor efetivamente registrado.

        Endereços são normalizados (minúsculas). Se `scope` for omitido, bloqueia
        somente o endereço (não o domínio).

        **Idempotente:** registrar a mesma regra novamente não gera erro; a
        resposta traz `already_existed: true`.

        **Sem RBAC:** não requer permissão específica além do token LIAM válido.
        A autorização (ser dono de uma caixa gerenciada) é verificada no handler.
      tags:
        - Email
      security:
        - liamBearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [sender]
              properties:
                sender:
                  type: string
                  maxLength: 320
                  description: Endereço de e-mail ou domínio do remetente a rejeitar.
                  example: "remetente-indesejado@dominio-exemplo.com.br"
                scope:
                  type: string
                  enum:
                    - address
                    - domain
                  description: |
                    Abrangência do bloqueio. Padrão `address` (apenas o e-mail).
                  example: "address"
      responses:
        "200":
          description: Remetente adicionado à lista de rejeição
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/EmailRejectListEntryResponse"
              example:
                data:
                  sender: "remetente-indesejado@dominio-exemplo.com.br"
                  scope: "address"
                  recipient: "usuario@dominio-exemplo.com.br"
                  type: "reject"
                  created_at: "2026-06-30T10:15:00"
                  already_existed: false
        "401":
          description: Token inválido ou ausente
        "403":
          description: |
            Identity não corresponde a uma conta de e-mail gerenciada
            (code `3000030`)
        "406":
          description: |
            `sender` inválido para o escopo informado (code `3000031`)
        "400":
          description: Parâmetro inválido — `sender` ausente (code `107`)

  /email/reject_list/remove/v1.0:
    post:
      summary: Remover remetente da lista de rejeição da própria caixa
      operationId: email_reject_list_remove
      description: |
        Remove um remetente (endereço ou domínio) da **lista de rejeição** da
        caixa do usuário autenticado. A regra é identificada pelos mesmos
        parâmetros usados no registro (`sender` + `scope`), recalculados sobre
        a caixa autenticada.

        **Auto-serviço:** opera sempre sobre a caixa cujo dono é a identity
        autenticada (`identity.email`). Não há como remover regras de terceiros.

        **Idempotente:** remover uma regra inexistente **não** é erro — a
        resposta traz `removed: false`. `scope` deve corresponder ao usado no
        registro original (`address` é o padrão).
      tags:
        - Email
      security:
        - liamBearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [sender]
              properties:
                sender:
                  type: string
                  maxLength: 320
                  description: Endereço de e-mail ou domínio a desbloquear.
                  example: "remetente-indesejado@dominio-exemplo.com.br"
                scope:
                  type: string
                  enum:
                    - address
                    - domain
                  description: Abrangência da regra. Padrão `address`.
                  example: "address"
      responses:
        "200":
          description: Operação concluída (verifique `removed`)
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/EmailRejectListRemoveResponse"
              example:
                data:
                  sender: "remetente-indesejado@dominio-exemplo.com.br"
                  scope: "address"
                  recipient: "usuario@dominio-exemplo.com.br"
                  type: "reject"
                  created_at: "2026-06-30T10:15:00"
                  removed: true
        "401":
          description: Token inválido ou ausente
        "403":
          description: |
            Identity não corresponde a uma conta de e-mail gerenciada
            (code `3000030`)
        "406":
          description: |
            `sender` inválido para o escopo informado (code `3000031`)
        "400":
          description: Parâmetro inválido — `sender` ausente (code `107`)

  /email/reject_list/list/v1.0:
    post:
      summary: Listar a lista de rejeição da própria caixa
      operationId: email_reject_list_list
      description: |
        Retorna todas as regras da **lista de rejeição** cuja caixa de destino
        é a do usuário autenticado, da mais recente para a mais antiga.

        **Auto-serviço:** não recebe parâmetros — o escopo é determinado pela
        identity autenticada (`identity.email`). Lista apenas as regras que o
        próprio dono pode gerenciar (escopadas à sua caixa).
      tags:
        - Email
      security:
        - liamBearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
      responses:
        "200":
          description: Lista de rejeição resolvida
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/EmailRejectListResponse"
              example:
                data:
                  recipient: "usuario@dominio-exemplo.com.br"
                  count: 2
                  entries:
                    - sender: "remetente-indesejado@dominio-exemplo.com.br"
                      scope: "address"
                      recipient: "usuario@dominio-exemplo.com.br"
                      type: "reject"
                      created_at: "2026-06-30T10:15:00"
                    - sender: "dominio-spam-exemplo.com.br"
                      scope: "domain"
                      recipient: "usuario@dominio-exemplo.com.br"
                      type: "reject"
                      created_at: "2026-06-29T08:00:00"
        "401":
          description: Token inválido ou ausente
        "403":
          description: |
            Identity não corresponde a uma conta de e-mail gerenciada
            (code `3000030`)

  /event/list/v1.0:
    post:
      summary: Listar eventos (cursor incremental)
      operationId: event_list
      description: |
        Retorna eventos ordenados cronologicamente a partir do cursor
        `after_event_id`. Projetado para consumo incremental: o consumidor
        mantém seu cursor e avança com `last_event_id` retornado a cada página.

        **Consistência de entrega:** o endpoint garante que nenhum evento seja
        ignorado acidentalmente. Em condições de alta concorrência pode retornar
        HTTP 409 — o consumidor deve aguardar alguns segundos e repetir a chamada
        **sem avançar o cursor**.

        **Cursor vs. filtro de tipo:** `last_event_id` pode avançar mesmo quando
        nenhum evento do tipo solicitado estiver presente na página — isso é
        esperado e não indica perda de dados.

        **Permissões por categoria:**
        - `lam3.event.list` — portão de entrada (obrigatório).
        - `lam3.event.<categoria>.list` — controla quais categorias são retornadas:
          - `email`, `domain`, `dkim`, `contract`, `customer`, `distribution_list`.
        - Se `event_type_ids` informado: valida que o caller tem permissão para
          **todas** as categorias envolvidas (403 se negado).
        - Se `event_type_ids` ausente: retorna apenas categorias autorizadas.
      tags:
        - Eventos
      security:
        - liamBearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                after_event_id:
                  type: integer
                  description: Cursor — retorna eventos com event_id > este valor. Default 0.
                  example: 1359000
                event_type_ids:
                  type: array
                  items:
                    type: string
                  description: Filtro por tipo(s) de evento. Se ausente, retorna todos os tipos autorizados.
                  example: ["EV_EMAIL_ACCOUNT_ADD", "EV_EMAIL_ACCOUNT_DELETE"]
                limit:
                  type: integer
                  description: Tamanho máximo da página (default 500, máx 2000).
                  example: 500
                include_archived:
                  type: boolean
                  description: Se true, inclui eventos arquivados (default false).
                  example: false
      responses:
        "200":
          description: Lista de eventos
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      events:
                        type: array
                        items:
                          $ref: "#/components/schemas/EventRecord"
                      last_event_id:
                        type: integer
                        description: |
                          Use como `after_event_id` na próxima chamada para continuar
                          a partir deste ponto.
                      has_more:
                        type: boolean
                        description: true se pode haver mais eventos além desta página.
              example:
                data:
                  events:
                    - event_id: 1359001
                      event_type_id: "EV_EMAIL_ACCOUNT_ADD"
                      event_date: "2026-05-08T14:00:00"
                      event_archived: false
                      event_data:
                        mail_account_id: 45257
                        domain_name: "cliente.com.br"
                  last_event_id: 1359001
                  has_more: false
        "401":
          description: Token inválido ou ausente
        "403":
          description: |
            Sem permissão. Códigos LAM3 possíveis:
            - `113` — sem permissão para a(s) categoria(s) solicitada(s)
        "406":
          description: |
            Validação de negócio. Códigos LAM3 possíveis:
            - `107` — tipo(s) de evento desconhecido(s) em `event_type_ids`
        "409":
          description: |
            Consistência temporária não garantida — repita a chamada em alguns
            segundos **sem avançar o cursor**. Código LAM3: `3005002`

  /event/get/v1.0:
    post:
      summary: Buscar evento por ID
      operationId: event_get
      description: |
        Retorna um evento pelo `event_id`. Verifica que o caller tem permissão
        para a categoria do evento (`lam3.event.<categoria>.read`).

        Permissões:
        - `lam3.event.read` — portão de entrada (obrigatório).
        - `lam3.event.<categoria>.read` — obrigatório para a categoria do evento
          solicitado (`email`, `domain`, `dkim`, `contract`, `customer`, `distribution_list`).
      tags:
        - Eventos
      security:
        - liamBearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [event_id]
              properties:
                event_id:
                  type: integer
                  description: ID do evento a ser buscado.
                  example: 1359001
      responses:
        "200":
          description: Evento encontrado
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/EventRecord"
              example:
                data:
                  event_id: 1359001
                  event_type_id: "EV_EMAIL_ACCOUNT_ADD"
                  event_date: "2026-05-08T14:00:00"
                  event_archived: false
                  event_data:
                    mail_account_id: 45257
                    domain_name: "cliente.com.br"
        "401":
          description: Token inválido ou ausente
        "403":
          description: |
            Sem permissão. Código LAM3:
            - `113` — sem permissão para a categoria do evento
        "404":
          description: |
            Evento não encontrado. Código LAM3:
            - `3005001` — event_id inexistente

  /ssl/list/v1.0:
    post:
      summary: Listar os certificados SSL do cliente
      operationId: ssl_list
      description: |
        Lista os certificados das contas a que o token tem acesso (restrito ao que o
        cliente pode ver — o escopo é resolvido por tenant, como em `domain/list`).
        Visão **somente leitura** e **segura**: traz nome comum, domínios, nível
        (DV/OV), método de validação, vigência da cobertura, vencimento do certificado
        atual e o status da emissão em andamento — **sem** qualquer dado interno
        (fornecedor, custo, pagamento, erros internos).

        Filtros opcionais: `customer_id` (deve ser acessível ao token) e `status_filter`
        (status do contrato). Permissão (por tenant): `lam3.ssl.certificate.list`.
      tags:
        - Certificados SSL
      security:
        - liamBearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                customer_id:
                  type: string
                  description: Opcional. Restringe a um cliente específico (acessível ao token).
                status_filter:
                  type: string
                  description: "Opcional. Filtra pelo status do contrato (ex.: active, expired)."
      responses:
        "200":
          description: Lista de certificados acessíveis.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      certificates:
                        type: array
                        items:
                          type: object
                          properties:
                            ssl_certificate_id: { type: string }
                            account_id: { type: string }
                            common_name: { type: string }
                            san_list: { type: array, items: { type: string } }
                            validation_method: { type: string, enum: [email, dns, http] }
                            validation_level: { type: string, enum: [dv, ov] }
                            status: { type: string }
                            covered_from: { type: string }
                            covered_until: { type: string }
                            certificate_valid_to: { type: string }
                            days_to_expiry: { type: integer, nullable: true }
                            issuance:
                              type: object
                              nullable: true
                              properties:
                                issuance_id: { type: string }
                                status:
                                  type: string
                                  enum: [processing, awaiting_validation, issuing, issued, error, superseded, expired]
                                valid_from: { type: string }
                                valid_to: { type: string }
                                can_retry: { type: boolean }
        "401":
          description: Token inválido ou ausente
        "403":
          description: "Sem permissão / nenhum tenant acessível. Código LAM3: `3003010`"

  /ssl/payment/confirm/v1.0:
    post:
      summary: Confirmar pagamento e liberar certificado para emissão
      operationId: ssl_payment_confirm
      description: |
        Confirma o pagamento de um contrato SSL: muda o status para `available`
        (liberado para emissão), registra o prazo pago e dispara a notificação
        aos contatos do cliente.

        **Vigência (comercial):** `paid_years` estende a vigência paga
        (`covered_until`). Em renovação, soma ao `covered_until` vigente; em
        certificado novo/expirado, a vigência é ancorada na emissão real (a
        emissão recalibra). `covered_until` explícito tem precedência.

        **Idempotência:** informe `payment_reference` (string opaca definida pelo
        integrador). Se a mesma referência já foi aplicada, a parte financeira
        **não** é reaplicada (não estende em dobro sob retry); a notificação
        pendente ainda é completada.

        Permissão: `lam3.ssl.certificate.payment_confirm`.
      tags:
        - Certificados SSL
      security:
        - liamBearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ssl_certificate_id]
              properties:
                ssl_certificate_id:
                  type: string
                  description: ID do contrato SSL (ssl_certificate).
                  example: "00000000-0000-0000-0000-000000000000"
                paid_years:
                  type: integer
                  description: Prazo pago em anos. Estende a vigência comercial.
                  example: 1
                covered_until:
                  type: string
                  format: date
                  description: Data final explícita da vigência (YYYY-MM-DD). Precede paid_years.
                  example: "2027-12-31"
                payment_reference:
                  type: string
                  description: Chave de idempotência do pagamento (string opaca do integrador).
                  example: "PGTO-0001"
                notify:
                  type: boolean
                  description: Enviar a notificação ao cliente (default true). False = só libera.
                  example: true
      responses:
        "200":
          description: Pagamento confirmado
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      ssl_certificate_id:
                        type: string
                      ssl_certificate_common_name:
                        type: string
                      ssl_certificate_status:
                        type: string
                      ssl_certificate_covered_until:
                        type: string
                        nullable: true
                      notification:
                        type: object
                        description: Resultado do processamento da notificação.
              example:
                data:
                  ssl_certificate_id: "00000000-0000-0000-0000-000000000000"
                  ssl_certificate_common_name: "*.exemplo.com.br"
                  ssl_certificate_status: "available"
                  ssl_certificate_covered_until: "2027-12-31"
                  notification:
                    sent: 1
                    skipped: false
        "401":
          description: Token inválido ou ausente
        "404":
          description: |
            Certificado não encontrado. Código LAM3: `112`
        "406":
          description: |
            Validação de input. Código LAM3: `107` (ex.: covered_until em formato inválido)
        "422":
          description: |
            Cliente sem contato técnico/master com e-mail válido. Código LAM3: `3006002`
        "502":
          description: |
            Falha ao registrar a notificação no TEP — refaça a chamada
            (idempotente via payment_reference). Código LAM3: `3006003`

  /ssl/issuance/notify/v1.0:
    post:
      summary: Disparar/reenviar a notificação de liberação SSL
      operationId: ssl_issuance_notify
      description: |
        Envia (ou reenvia) a notificação de liberação/emissão aos contatos do
        cliente, sem alterar o pagamento. Idempotente: por padrão **não** reenvia
        se já enviada — use `force=true` para reenviar. Exige o contrato pago
        (`status=available`).

        Permissão: `lam3.ssl.certificate.notify`.
      tags:
        - Certificados SSL
      security:
        - liamBearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ssl_certificate_id]
              properties:
                ssl_certificate_id:
                  type: string
                  description: ID do contrato SSL (ssl_certificate).
                  example: "00000000-0000-0000-0000-000000000000"
                force:
                  type: boolean
                  description: Reenviar mesmo se já notificado (default false).
                  example: false
      responses:
        "200":
          description: Notificação processada
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      ssl_certificate_id:
                        type: string
                      ssl_certificate_common_name:
                        type: string
                      ssl_certificate_status:
                        type: string
                      notification:
                        type: object
              example:
                data:
                  ssl_certificate_id: "00000000-0000-0000-0000-000000000000"
                  ssl_certificate_common_name: "*.exemplo.com.br"
                  ssl_certificate_status: "available"
                  notification:
                    sent: 1
                    skipped: false
        "401":
          description: Token inválido ou ausente
        "404":
          description: |
            Certificado não encontrado. Código LAM3: `112`
        "409":
          description: |
            Contrato não está liberado/pago. Código LAM3: `3006001`
        "422":
          description: |
            Cliente sem contato técnico/master com e-mail válido. Código LAM3: `3006002`
        "502":
          description: |
            Falha ao registrar a notificação no TEP — refaça a chamada. Código LAM3: `3006003`

  /ssl/issue/v1.0:
    post:
      summary: Solicitar a emissão de um certificado SSL (gatilho)
      operationId: ssl_issue
      description: |
        **Gatilho** da emissão: registra o pedido e retorna na hora — **não** dirige a
        máquina de estados nem produz efeito externo. Cria/retoma a emissão em `draft`
        e persiste a escolha de validação (método + aprovador). A emissão física é
        conduzida pelo **engine** (em segundo plano); o cliente apenas **acompanha o
        status** da emissão.

        Exige o contrato liberado (`status=available`).

        **Produto e prazo são definidos pelo CONTRATO (imutáveis)** — não são
        parâmetros. Por emissão escolhe-se apenas o método de validação do domínio.

        **Validação do domínio (DCV):** `email` (mensagem ao endereço aprovador),
        `dns` (CNAME — se a zona for gerenciada pela Laniway, publicamos o registro
        automaticamente; senão, enviamos as instruções) ou `http`. O conjunto aceito
        depende do produto do contrato; um método não suportado retorna `3006006`.

        O parâmetro `dry_run` é **global** (ver a introdução da API): aqui valida o
        pedido e retorna um *preview* sem registrar nada.

        Permissão: `lam3.ssl.certificate.issue`.
      tags:
        - Certificados SSL
      security:
        - liamBearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ssl_certificate_id]
              properties:
                ssl_certificate_id:
                  type: string
                  description: ID do contrato SSL (ssl_certificate).
                  example: "00000000-0000-0000-0000-000000000000"
                dcv_method:
                  type: string
                  enum: [email, dns, http]
                  description: |
                    Método de validação do domínio (padrão = validation_method do
                    contrato, senão email). Método não suportado pelo produto → 3006006.
                  example: "dns"
                approver_email:
                  type: string
                  description: E-mail aprovador para DCV por e-mail (padrão = admin@<domínio>).
                  example: "admin@exemplo.com.br"
      responses:
        "200":
          description: |
            Pedido registrado (emissão em `draft`). O engine conduz a emissão em
            segundo plano — acompanhe `issuance_status`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      ssl_certificate_id:
                        type: string
                      ssl_certificate_common_name:
                        type: string
                      issuance_id:
                        type: string
                      issuance_status:
                        type: string
                        description: draft | pending_csr | pending_validation | issued | error
              examples:
                scheduled:
                  summary: Pedido registrado
                  value:
                    data:
                      ssl_certificate_id: "00000000-0000-0000-0000-000000000000"
                      ssl_certificate_common_name: "*.exemplo.com.br"
                      issuance_id: "11111111-1111-1111-1111-111111111111"
                      issuance_status: "draft"
                dry_run:
                  summary: dry_run (preview, nada registrado)
                  value:
                    data:
                      ssl_certificate_id: "00000000-0000-0000-0000-000000000000"
                      ssl_certificate_common_name: "*.exemplo.com.br"
                      dry_run: true
                      dcv_method: "dns"
        "401":
          description: Token inválido ou ausente
        "404":
          description: |
            Certificado não encontrado. Código LAM3: `112`
        "409":
          description: |
            Contrato não está liberado/pago. Código LAM3: `3006001`
        "422":
          description: |
            Método de validação (dcv_method) não suportado para este produto (`3006006`),
            ou contrato com validação de organização sem os dados completos (`3006007`).

  /ssl/resend/v1.0:
    post:
      summary: Reenviar o e-mail da emissão (entrega ou DCV)
      operationId: ssl_resend
      description: |
        Solicita o **reenvio** do e-mail da emissão mais recente do contrato — útil
        quando o cliente não recebeu/abriu corretamente. Liga uma flag; o **engine**
        reenvia no próximo tick, usando o material já no S3 e os dizeres atuais:

        - emissão `issued` → reenvia a **entrega** (com o `.zip`: cert, cadeia, chave,
          `.pem` e `.pfx`);
        - emissão `pending_validation` → reenvia a **notificação de validação (DCV)**.

        Permissão: `lam3.ssl.certificate.notify`.
      tags:
        - Certificados SSL
      security:
        - liamBearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ssl_certificate_id]
              properties:
                ssl_certificate_id:
                  type: string
                  description: ID do contrato SSL (ssl_certificate).
                  example: "00000000-0000-0000-0000-000000000000"
                issuance_id:
                  type: string
                  description: |
                    Opcional. Reenvia ESSA emissão específica (deve pertencer ao
                    contrato). Sem ele, usa a emissão mais recente do contrato.
                  example: "11111111-1111-1111-1111-111111111111"
      responses:
        "200":
          description: Reenvio solicitado (processado pelo engine no próximo tick).
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      ssl_certificate_id:
                        type: string
                      issuance_id:
                        type: string
                      issuance_status:
                        type: string
                      resend_requested:
                        type: boolean
              example:
                data:
                  ssl_certificate_id: "00000000-0000-0000-0000-000000000000"
                  issuance_id: "11111111-1111-1111-1111-111111111111"
                  issuance_status: "issued"
                  resend_requested: true
        "401":
          description: Token inválido ou ausente
        "404":
          description: |
            Certificado ou emissão não encontrado. Código LAM3: `112`

  /ssl/retry/v1.0:
    post:
      summary: Recolocar na fila uma emissão parada
      operationId: ssl_retry
      description: |
        Recoloca uma emissão **parada** de volta na fila — o engine retoma no próximo
        tick, **do passo em que parou**. Uma emissão é dada como parada (e não é mais
        retentada automaticamente) após **48 horas** de tentativas sem sucesso; este
        endpoint é o caminho para retomá-la depois que a causa foi resolvida.

        - emissão parada → restaura o passo e agenda imediatamente;
        - emissão ainda em andamento → apenas antecipa o próximo retry;
        - emissão já concluída → nada a retentar.

        Permissão: `lam3.ssl.certificate.issue`.
      tags:
        - Certificados SSL
      security:
        - liamBearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ssl_certificate_id]
              properties:
                ssl_certificate_id:
                  type: string
                  description: ID do contrato SSL (ssl_certificate).
                  example: "00000000-0000-0000-0000-000000000000"
                issuance_id:
                  type: string
                  description: |
                    Opcional. Retenta ESSA emissão específica (deve pertencer ao
                    contrato). Sem ele, usa a emissão mais recente do contrato.
                  example: "11111111-1111-1111-1111-111111111111"
      responses:
        "200":
          description: Emissão recolocada na fila (ou nada a retentar).
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      ssl_certificate_id: { type: string }
                      issuance_id: { type: string }
                      previous_status: { type: string }
                      issuance_status: { type: string }
                      requeued: { type: boolean }
        "401":
          description: Token inválido ou ausente
        "404":
          description: |
            Certificado ou emissão não encontrado. Código LAM3: `112`

  /ssl/approval/resend/v1.0:
    post:
      summary: Reenviar o e-mail de aprovação de domínio (mesmo aprovador)
      operationId: ssl_approval_resend
      description: |
        Pede ao provedor para **reenviar** o e-mail de aprovação de domínio (DCV por
        e-mail) ao **mesmo** endereço aprovador já registrado — útil quando o cliente
        não recebeu/perdeu a mensagem. Aplica-se a emissões **aguardando validação por
        e-mail**. **Trocar** o aprovador não é suportado (exigiria uma nova emissão).

        Autorização (token do cliente): `lam3.ssl.certificate.self_manage` no contrato
        — só age sobre certificado do próprio cliente.
      tags:
        - Certificados SSL
      security:
        - liamBearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ssl_certificate_id]
              properties:
                ssl_certificate_id:
                  type: string
                  example: "00000000-0000-0000-0000-000000000000"
                issuance_id:
                  type: string
                  description: Opcional. A emissão específica (deve ser do contrato). Sem ele, a mais recente.
      responses:
        "200":
          description: Reenvio do e-mail de aprovação solicitado ao provedor.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      ssl_certificate_id: { type: string }
                      issuance_id: { type: string }
                      approver_email: { type: string }
                      resent: { type: boolean }
        "401":
          description: Token inválido ou ausente
        "403":
          description: "Sem permissão para este certificado. Código LAM3: `3003010`"
        "409":
          description: "Emissão não está aguardando validação por e-mail. Código LAM3: `3006008`"
        "404":
          description: "Certificado/emissão não encontrado. Código LAM3: `112`"

  /ssl/contact/list/v1.0:
    post:
      summary: Listar contatos de exceção do contrato
      operationId: ssl_contact_list
      description: |
        Lista os **contatos de exceção** de um contrato. São usados apenas em casos
        especiais (ex.: cliente final faturado por um intermediário do grupo, que não
        existe como conta) — quando há contatos aqui, eles recebem as notificações do
        certificado no lugar dos contatos da conta. Lista vazia = usa os da conta.

        Permissão: `lam3.ssl.certificate.contact_manage`.
      tags:
        - Certificados SSL
      security:
        - liamBearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ssl_certificate_id]
              properties:
                ssl_certificate_id:
                  type: string
                  description: ID do contrato SSL (ssl_certificate).
                  example: "00000000-0000-0000-0000-000000000000"
      responses:
        "200":
          description: Lista de contatos de exceção.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      ssl_certificate_id: { type: string }
                      ssl_certificate_common_name: { type: string }
                      contacts:
                        type: array
                        items:
                          type: object
                          properties:
                            contact_id: { type: string }
                            role: { type: string, enum: [notification, ov_approver] }
                            name: { type: string }
                            email: { type: string }
                            phone: { type: string }
                            is_primary: { type: boolean }
                            ov_title: { type: string }
                            status: { type: string, enum: [active, inactive] }
        "401":
          description: Token inválido ou ausente
        "404":
          description: "Certificado não encontrado. Código LAM3: `112`"

  /ssl/contact/upsert/v1.0:
    post:
      summary: Criar/atualizar contato de exceção do contrato
      operationId: ssl_contact_upsert
      description: |
        Cria (sem `contact_id`) ou atualiza (com `contact_id`, que deve pertencer ao
        contrato) um contato de exceção. `is_primary=true` define o destinatário **TO**;
        os demais entram em **CC**. `role=ov_approver` reserva o aprovador para validação
        organizacional (uso futuro).

        Permissão: `lam3.ssl.certificate.contact_manage`.
      tags:
        - Certificados SSL
      security:
        - liamBearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ssl_certificate_id, email]
              properties:
                ssl_certificate_id:
                  type: string
                  description: ID do contrato SSL.
                  example: "00000000-0000-0000-0000-000000000000"
                contact_id:
                  type: string
                  description: Opcional. Quando presente, atualiza esse contato (deve ser do contrato).
                email:
                  type: string
                  description: E-mail do contato (obrigatório).
                name:
                  type: string
                  description: Nome do contato.
                phone:
                  type: string
                  description: Telefone do contato.
                role:
                  type: string
                  enum: [notification, ov_approver]
                  description: "Papel (padrão = notification)."
                is_primary:
                  type: boolean
                  description: "Quando true, é o destinatário principal (TO)."
                ov_title:
                  type: string
                  description: Cargo do aprovador (apenas role=ov_approver).
                status:
                  type: string
                  enum: [active, inactive]
                  description: "Situação (padrão = active)."
      responses:
        "200":
          description: Contato criado ou atualizado.
        "401":
          description: Token inválido ou ausente
        "404":
          description: "Certificado/contato não encontrado. Código LAM3: `112`"
        "422":
          description: "E-mail ausente ou papel/situação inválidos. Código LAM3: `107`"

  /ssl/contact/delete/v1.0:
    post:
      summary: Remover contato de exceção do contrato
      operationId: ssl_contact_delete
      description: |
        Remove um contato de exceção do contrato.

        Permissão: `lam3.ssl.certificate.contact_manage`.
      tags:
        - Certificados SSL
      security:
        - liamBearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ssl_certificate_id, contact_id]
              properties:
                ssl_certificate_id:
                  type: string
                  example: "00000000-0000-0000-0000-000000000000"
                contact_id:
                  type: string
                  example: "22222222-2222-2222-2222-222222222222"
      responses:
        "200":
          description: Contato removido.
        "401":
          description: Token inválido ou ausente
        "404":
          description: "Certificado/contato não encontrado. Código LAM3: `112`"

  /ssl/reconcile/v1.0:
    post:
      summary: Engine de emissão SSL — avançar uma transição (interno)
      operationId: ssl_reconcile
      description: |
        Endpoint **interno** do engine de emissão. Avança **uma** emissão **um passo
        de estado** por chamada (a aquisição commita antes da ativação) e retorna
        `remaining` (emissões ainda pendentes). É chamado por um agendador (loop do
        servidor, e futuramente um agendador externo em round-robin entre instâncias),
        que reinvoca enquanto `remaining > 0`. Habilitado por `LAM3_SSL_ENGINE_ENABLED`.

        Permissão: `lam3.ssl.engine.run`. Não aceita `dry_run`.
      tags:
        - Certificados SSL
      security:
        - liamBearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
      responses:
        "200":
          description: Resultado do passo (ou estado ocioso/desabilitado).
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      status:
                        type: string
                        description: ok | idle | busy | disabled
                      action:
                        type: string
                        description: started | activated | retry | noop (quando status=ok)
                      issuance_id:
                        type: string
                      state:
                        type: string
                      remaining:
                        type: integer
                        description: Emissões ainda vencidas (o agendador reinvoca enquanto > 0).
              example:
                data:
                  status: "ok"
                  action: "started"
                  issuance_id: "11111111-1111-1111-1111-111111111111"
                  state: "pending_csr"
                  remaining: 1
        "401":
          description: Token inválido ou ausente
        "403":
          description: Identidade sem a permissão `lam3.ssl.engine.run`

