Integração com o Lacuna Signer
Passo a passo para o operador fazer um perfil assinar pelo Lacuna Signer, em vez de usar um certificado mantido localmente. A assinatura com certificado local (PFX / PKCS#11 / repositório do Windows) e a assinatura pelo Lacuna Signer coexistem, cada uma em seu perfil — pastas monitoradas diferentes podem usar métodos de assinatura diferentes na mesma instância.
Quando usar isto
Escolha Method = LacunaSigner para um perfil quando:
- Uma pessoa (e não um certificado mantido pelo servidor) precisa assinar cada documento — por exemplo, contratos com contra-assinatura, contratos de trabalho, documentação de admissão de RH.
- A identidade do signatário é a do participante, e não a do serviço. Cada documento despachado pertence ao participante, do lado do Signer.
- A trilha de auditoria que você quer é a que o Signer mantém (identidade do signatário, evidência da assinatura, motivos de recusa, expiração).
Escolha Method = Local (o padrão) quando:
- A assinatura é do serviço — assinatura automatizada de notas fiscais com o certificado de assinatura da empresa, assinatura de NF-e em tempo de execução em um token PKCS#11, contra-assinatura em lote.
- O certificado fica no host (PFX / HSM / repositório do Windows) e não há uma pessoa no circuito.
Os dois podem rodar lado a lado. Uma única instância pode monitorar input/contracts/ (LacunaSigner) e
input/nfe/ (PKCS#11 local) ao mesmo tempo.
Escolha Method = SignerFolder — a direção oposta — quando uma pessoa cria documentos no Lacuna
Signer e indica como participante o titular do certificado da organização, e a assinatura desse
participante deve chegar sem que ninguém precise tocar em um token. Veja
Assinando em uma pasta do Lacuna Signer abaixo.
Resumo da arquitetura
input/ ─▶ Observador ─▶ Queued ─▶ worker reivindica
│
profile.Method? ───┤
│
Local ───────────────────────────────▶ assina no slot ─▶ Verifying ─▶ Completed
│
LacunaSigner ─▶ upload + cria documento ─▶ AwaitingSigner (slot de concorrência LIBERADO)
│
tique do worker de consulta ──┤
│
Pending → continua AwaitingSigner
Concluded → baixa os bytes ─▶ Verifying ─▶ Completed
Refused/Expired/Canceled → Failed
timeout → Failed
São dois workers que cooperam, em vez de um:
- O worker do pipeline reivindica jobs
Queuede, em perfis LacunaSigner, apenas os despacha ao Signer (upload + criação do documento) e os passa paraAwaitingSigner. O slot do pipeline é liberado imediatamente após o despacho — o job agora está retido do lado remoto, e o worker fica livre para pegar o próximo item. - Um worker de polling separado acorda a cada
Signer:PollIntervalSeconds(padrão 30 s) e percorre todas as linhasAwaitingSigner. Para cada linha, ele verifica o status do documento na API do Signer; os documentos concluídos são baixados e passam pela mesma etapa final de verificar → criptografar → promover que o caminho Local usa.
Essa divisão importa: ocupar um slot de Pipeline:MaxConcurrency enquanto uma pessoa leva dias para
assinar anularia completamente o propósito da fila.
A máquina de estados, estendida
Em perfis LacunaSigner, o AwaitingSigner se encaixa entre Processing e Verifying:
Queued ─▶ Processing ─┬─ assinatura local ok ────────▶ Verifying ─▶ Completed
│ └▶ Failed
└─ despachado ao Signer ─▶ AwaitingSigner
│
concluído → download ───────┼──▶ Verifying ─▶ Completed
recusado/expirado/timeout ──┴──▶ Failed
cancel do operador ────────────▶ Canceled (cancelamento remoto em melhor esforço)
Perfis que usam apenas o método Local nunca entram em AwaitingSigner. Perfis LacunaSigner nunca
seguem o caminho local direto Processing → Verifying.
Configuração
Signer:* — um tenant por host
A conexão com o Signer é global — um endpoint + uma chave de API para o host, compartilhados por
todos os perfis que usam Method = LacunaSigner ou Method = SignerFolder. Os dois métodos leem
metades diferentes do restante do bloco:
| Chave | Tipo | Padrão | Override por env | Usada por | Obrigatória quando |
|---|---|---|---|---|---|
Signer:Endpoint | string | "" | Signer__Endpoint | os dois métodos | Algum perfil usa LacunaSigner ou SignerFolder, ou qualquer outra parte de Signer: está definida. Padrão na nuvem: https://signer.lacunasoftware.com. |
Signer:ApiKey | string | "" | Signer__ApiKey | os dois métodos | REQUIRED, SECRET, mesma condição. Formato: application-id|secret. |
Signer:PollIntervalSeconds | int | 30 | Signer__PollIntervalSeconds | LacunaSigner | opcional — a frequência com que o worker de polling percorre as linhas AwaitingSigner. 1–3600. |
Signer:TimeoutHours | int | 168 (7 dias) | Signer__TimeoutHours | LacunaSigner | opcional — quanto tempo um documento despachado pode esperar pelo participante. 1–8760. |
Signer:MaxConsecutiveApiFailures | int | 5 | Signer__MaxConsecutiveApiFailures | LacunaSigner | opcional — o limite por documento do polling, descrito abaixo. 1–100. |
Signer:FolderSweepIntervalSeconds | int | 300 (5 minutos) | Signer__FolderSweepIntervalSeconds | SignerFolder | opcional — a frequência com que cada pasta do Signer vinculada é listada; é o máximo que um documento espera até este host percebê-lo. 60–86400. |
Signer:WebhookSecret | string | "" (notificações desligadas) | Signer__WebhookSecret | SignerFolder | opcional, SECRET — o token Bearer que o Signer apresenta em uma notificação. Pelo menos 32 caracteres. Defini-lo conta como definir o bloco, então Signer:Endpoint e Signer:ApiKey passam a ser obrigatórias. |
Um host que precisa de apenas um método pode deixar as chaves do outro nos valores padrão: nada as lê. Todas as chaves, com seus limites e recusas, também estão em Configuração.
O validador só age sobre esta seção quando ela existe — omita Signer:* por inteiro e nada aqui é
exigido, que é o que acontece em uma implantação puramente Local. Escreva qualquer parte dela e o bloco
inteiro é validado.
Signer:* é validado por si sóAté a 2.0.x, o bloco só era validado quando algum perfil selecionava Method = LacunaSigner. Os perfis
agora ficam no banco de dados operacional e podem ser trocados para o Lacuna Signer pelo dashboard sem
reinicialização, então a regra passou a valer ao contrário:
- Um bloco
Signer:preenchido pela metade impede o boot, mesmo que nenhum perfil o use — um endpoint sem chave de API deixado para depois, por exemplo. A mensagem cita as duas chaves e sugere remover a seção como solução. - Selecionar
Method = LacunaSigneré recusado quando o host não tem configuraçõesSigner:*— no boot, para um perfil ainda importado da configuração pelo seed (carga inicial), e na página do perfil, para um perfil que está sendo salvo. - É a presença das configurações
Signer:*no host que inicia o gateway do assinador remoto e o worker de polling, então um perfil trocado para o Lacuna Signer depois do boot começa a despachar sem reinicialização. Em um host com o bloco inteiro definido e nenhum perfil que o use, o worker de polling roda e não encontra nada a fazer a cada intervalo — remova a seção se este host assina tudo localmente.
Defina-os como Signer__ApiKey e Signer__WebhookSecret no bulksigner.env (Linux) / em uma variável
de ambiente de máquina (Windows) / no .env (Docker). Os valores literais são mascarados nos logs.
Um perfil que seleciona Method = SignerFolder é recusado em um host sem configurações Signer:*
exatamente como um perfil LacunaSigner, e são essas mesmas configurações que iniciam a varredura de
pastas do Signer. Um perfil SignerFolder já armazenado, em um host que depois perdeu as configurações,
fica degradado — nunca impede o boot.
Escolhendo o método pelo dashboard
É aqui que você escolhe o método em uma implantação em execução. O Signing:Profiles[] é um seed de
uso único, importado no primeiro boot (veja
Configuração); por isso, a próxima
seção descreve como é um seed, e não onde você muda um perfil.
O painel Certificado em /profiles/{name} contém o método, e o formulário em /profiles/_new
também. Ele fica nesse painel, e não no de Comportamento, porque o método decide se o perfil tem ou
não uma chave local: escolha Lacuna Signer, e a origem do certificado e os dados dela são
substituídos pelos três campos do participante — nome, e-mail e identificador —, que são tudo o que
define de onde vem a assinatura de um perfil assim. Escolha Pasta do Lacuna Signer, e o certificado
continua, com um seletor de Pasta do Lacuna Signer acima dele — veja
Assinando em uma pasta do Lacuna Signer abaixo.
As direções da troca são diferentes, e o formulário informa qual se aplica antes de você salvar. A regra por trás cabe em uma frase: uma mudança espera uma reinicialização exatamente quando deixa o perfil precisando de uma chave que nenhuma instância em execução abriu.
| Troca | Quando passa a valer | O que acontece com o outro bloco |
|---|---|---|
| Local ou pasta do Signer → Lacuna Signer | No próximo job reivindicado. Sem reinicialização — o gateway roda em todo host que tem configurações Signer:*, e não só para os perfis que existiam no boot. | Os dados do certificado são apagados, inclusive a senha: uma credencial armazenada para uma chave que fica no serviço remoto é uma credencial que nada jamais usará. O vínculo com uma pasta do Signer é desfeito. |
| Lacuna Signer → Local ou pasta do Signer | Na próxima reinicialização, porque uma chave privada precisa ser aberta, e salvar o formulário não abre nenhuma. Até lá, o perfil aparece como degradado na própria página, e os jobs roteados para ele falham com profile.degraded. | O participante é apagado. |
| Local ↔ pasta do Signer, mesmo certificado | No próximo job reivindicado. Sem reinicialização — os dois assinam com a chave que este host já abriu, e ela é mantida. | Sair da pasta do Signer desfaz o vínculo com a pasta. |
| Qualquer salvamento que também mude um dado do certificado | Na próxima reinicialização, qualquer que seja o método — a regra do certificado não muda. | — |
As recusas acontecem ao salvar, no seu idioma de exibição: um participante sem algum dos três campos, um
e-mail sem @, e selecionar o Lacuna Signer em um host sem configurações Signer:* — a única recusa
cuja solução é uma mudança de configuração e uma reinicialização, e não um campo do formulário; por isso
a mensagem cita as chaves.
Os dígitos verificadores do identificador do participante não são validados. Diferentemente do CPF de um aprovador — que este produto grava nos próprios registros de auditoria —, este é entregue ao serviço remoto, e é o serviço que decide se conhece o participante.
Signing:Profiles[].Method + bloco Signer
Seleção do método por perfil como seed, importada no primeiro boot com a tabela de perfis vazia. O
padrão é Method = Local, então os perfis que já existiam não precisam de mudança.
"Signing": {
"Profiles": [
{
"Name": "contracts",
"Format": "Pades",
"Method": "LacunaSigner",
"Verify": true,
"Encrypt": false,
"ValidateCertificate": false,
"Signer": {
"Name": "Jack Bauer",
"Email": "jack.bauer@example.com",
"Identifier": "75502846369"
}
}
]
}
Validação no nível do perfil:
Method = LacunaSignerexige um blocoSigner:{Name, Email, Identifier}não vazio. O validador recusa blocos parciais.Method = LacunaSignerproíbe um blocoCertificate:*(não há certificado local envolvido).Method = LacunaSignerproíbeValidateCertificate = true(não há certificado local a validar).- As regras de
Method = Localnão mudam: o bloco de certificado é obrigatório, e o blocoSigneré ignorado se estiver presente. Method = LacunaSignernão pode ser combinado com uma regra de aprovação cujo conjunto de assinantes sejaProfileKeyAndApprovers— o assinador remoto receberia um envelope de assinaturas de aprovadores, e não o arquivo de pagamento. Veja Aprovações.
As mesmas regras fazem um salvamento ser recusado na página do perfil. O perfil default derivado
(criado pelo seed quando Signing:Profiles[] é omitido) é sempre Method = Local.
Fluxo do operador
- O operador coloca um arquivo em uma pasta monitorada por um perfil LacunaSigner (ou usa
POST /api/files?profile=contracts). - O observador / endpoint enfileira o job;
Status = Queued. - O worker do pipeline reivindica o próximo slot, passa o job para
Processinge então faz o upload e cria o documento no Signer. Em caso de sucesso, o job passa paraAwaitingSigner, com o id do documento remoto registrado; o slot é liberado. - O Signer envia um e-mail ao participante, que assina pela interface do Signer quando puder.
- O worker de polling executa um tique a cada
Signer:PollIntervalSeconds. Em cada tique, ele carrega todas as linhasAwaitingSigner, das mais antigas para as mais novas, e, para cada uma:- Pending → não mexe na linha.
- Concluded → baixa os bytes assinados, passa o job para
Verifying, executa a mesma etapa final de verificar → opcionalmente criptografar → promover e passa o job paraCompleted. - Refused / Expired / Canceled → passa o job para
Failedcomsigner.document-rejected. - Timeout local (
AwaitingSignerpor mais tempo queSigner:TimeoutHours) → passa o job paraFailedcomsigner.timeout. O documento remoto é deixado como está, do lado do Signer.
O dashboard exibe AwaitingSigner como um status próprio (chip amarelo, ícone de ampulheta). A página
de detalhe do job mostra o id do documento remoto e a hora do despacho, e um card de estatística
Aguardando assinatura aparece quando algum perfil LacunaSigner está configurado.
Semântica do cancelamento
Em perfis LacunaSigner, o cancelamento pelo operador passa a valer para {Queued, AwaitingSigner}.
Processing e Verifying continuam intocáveis.
Quando um operador cancela um job AwaitingSigner:
- O job passa para
Canceledlocalmente — mesmo handler, mesma trilha de auditoria. - Em seguida, o handler faz uma chamada de cancelamento remoto ao Signer, em melhor esforço. As falhas são registradas no log como Warning, mas não desfazem o cancelamento local.
- Se o cancelamento remoto falhou, o participante ainda pode ver o documento na caixa de entrada do
Signer. O job local está corretamente
Canceledde qualquer forma.
O botão Cancelar na página do job pede confirmação antes: o diálogo mostra o nome do arquivo e explica o que
o cancelamento faz a partir do status atual do job — no caso de um job aguardando o Lacuna Signer, que o
documento remoto dele é cancelado em melhor esforço. O POST /api/jobs/{id}/cancel não pede
confirmação.
O Limpar Jobs apaga todos os registros de jobs, qualquer que seja o status, inclusive
AwaitingSigner, mas não faz nenhuma chamada ao Lacuna Signer: um documento já despachado continua na
caixa de entrada do participante. Cancele esses jobs antes se o participante não deve assiná-los. Veja
Operação.
Desfazer o cancelamento local porque uma ida e volta de rede falhou deixaria o operador sem saber em que estado o job ficou e contradiria o princípio de que "cancelar encerra a questão". O caso de um documento remoto órfão é raro e inofensivo — o participante pode ignorar o e-mail, ou o operador pode fazer a limpeza na administração do Signer.
Falhas de API e o limite por job
A integração com o Signer distingue dois tipos de falha:
- Transitória — uma conexão recusada ou um timeout, um
5xx,408ou429, ou uma resposta que não vem da API do Signer (a página de erro de um proxy). O worker de polling incrementa um contador de falhas por documento e segue para a próxima linha. O contador zera na primeira chamada bem-sucedida. Quando oSigner:MaxConsecutiveApiFailuresé excedido para um único documento, aquele job falha comcode = signer.unreachable. As outras linhas não são afetadas. - Permanente — um 4xx que não se resolve com novas tentativas (chave de API inválida, documento
desconhecido, requisição malformada). O job falha imediatamente com
code = signer.unreachable.
Um reinício do processo zera os contadores de falha, que ficam em memória. Se a indisponibilidade de origem foi resolvida entre as falhas e o reinício, o polling é retomado normalmente no próximo boot.
Até a 2.15.x, só uma falha de conexão pura era reconhecida como o Signer estando inacessível. Um 5xx,
um 408 ou 429, a página de erro de um proxy ou o timeout do próprio cliente REST durante um polling
ou download era registrado no log e tentado de novo a cada tique, sem contar, até o
Signer:TimeoutHours. A partir da 2.16.0, cada um deles conta para o Signer:MaxConsecutiveApiFailures
e para o bulksigner_signer_api_errors_total; assim, uma indisponibilidade mais longa que o limite faz
o job falhar com signer.unreachable — use Tentar novamente quando o Signer voltar. Um erro HTTP que
a API do Signer não formatou, como um 4xx puro vindo de algo no caminho, agora faz o job falhar na hora
como permanente, como as recusas do próprio Signer sempre fizeram.
O Signer:MaxConsecutiveApiFailures protege apenas o caminho de polling. Uma falha transitória
durante o despacho faz o job falhar no primeiro erro, em vez de ser repetida até um limite — por
design, já que o despacho é uma única chamada curta no início do job. Se o seu endpoint do Signer é
instável a ponto de as falhas de despacho serem um problema, faça uma nova tentativa pelo dashboard ou
por REST (POST /api/jobs/{id}/retry) quando o serviço remoto voltar.
Recuperação após reinício — linhas AwaitingSigner NÃO são varridas
A varredura de recuperação na inicialização passa para Failed qualquer job travado em Processing /
Verifying (eles estavam em andamento quando o processo anterior morreu) — exceto um job
SignerFolder, que não tem arquivo e é
encerrado pela resposta do Lacuna Signer (veja
Quando a execução que o assinava termina). As linhas AwaitingSigner
são explicitamente excluídas — o trabalho está retido do lado remoto; varrê-las localmente perderia
dados que não cabe ao host invalidar. O worker de polling retoma o polling dessas linhas no próximo
boot, exatamente de onde parou.
O que chega a output/
Em perfis LacunaSigner, os bytes promovidos para output/ são os bytes que o Signer assinou — a
assinatura do participante sobre o documento original, baixada depois que o documento é concluído. As
etapas de verificação e criptografia rodam sobre esses bytes exatamente como rodariam em um perfil
Local; então:
Verify = true(padrão) — a assinatura é verificada com base na política configurada, após o download.Encrypt = true+Encryption:Enabled = true— os bytes baixados são criptografados com AES-256-GCM em um envelope BSENC v1; o texto claro nunca é gravado emoutput/.
Os arquivos de entrada originais só são apagados de input/ depois que a etapa de verificação é
concluída com sucesso — a mesma garantia do caminho Local.
Métricas
Instrumentos Prometheus específicos do Signer são expostos em /api/metrics:
| Métrica | Tipo | O que ela acompanha |
|---|---|---|
bulksigner_jobs_dispatched_to_signer_total{profile} | Counter | Despachos bem-sucedidos ao Signer, rotulados pelo nome do perfil. |
bulksigner_jobs_awaiting_signer | Gauge | Contagem atual de linhas AwaitingSigner. |
bulksigner_signer_poll_duration_seconds | Histogram | Duração, por tique, de uma passada completa sobre as linhas AwaitingSigner. |
bulksigner_signer_api_errors_total{op} | Counter | Falhas da API do Signer encontradas pelo worker de polling, rotuladas por operação (poll, download). Falhas de despacho e de cancelamento remoto ficam no registro do job — veja a nota sobre a assimetria acima. |
O método SignerFolder tem instrumentos próprios — veja Métricas e auditoria
abaixo.
No modo cluster
Com o modo cluster ligado, cada instância só faz polling no Lacuna Signer dos documentos que ela
mesma despachou, de modo que duas instâncias nunca baixam os mesmos bytes assinados. Há duas
consequências para dashboards e alertas: o bulksigner_jobs_awaiting_signer é por instância — some os
valores de toda a frota —, e um job que uma instância despachou antes de morrer é reatribuído a uma
sobrevivente pela varredura de assunção. Uma linha sem dono algum não recebe polling de ninguém. Veja
Alta disponibilidade para os detalhes e a solução.
Assinando em uma pasta do Lacuna Signer — SignerFolder
Um terceiro método de assinatura. Nada muda para um host sem perfil SignerFolder.
Tudo o que está acima segue uma direção: este host envia um arquivo ao Signer, e uma pessoa o assina
lá. Method = SignerFolder é a outra: uma pessoa cria um documento no Lacuna Signer, em uma
pasta vinculada a um perfil do Bulk Signer, com o titular do certificado do perfil como participante;
este host o assina com o certificado do próprio perfil, e o Signer incorpora a assinatura ao documento e
o guarda. Nenhum arquivo entra em input/, nada chega a output/, e o documento deixa de ser
acompanhado assim que o Signer aceita a assinatura.
Pessoa Lacuna Signer Bulk Signer
│ cria um documento na │ │
│ pasta vinculada, indicando │ │
│ o titular do certificado ▶ │ ── notificação (opcional) ────────▶ │
│ │ ◀── varredura / checagem: relê ──── │
│ │ │ vez do titular? → job
│ │ ◀── inicia com o certificado ────── │
│ │ ─── hash a assinar ───────────────▶ │
│ │ │ assina com a chave do perfil
│ │ ◀── conclui com a assinatura ────── │
│ │ incorpora, guarda o documento │
O perfil
Ele mantém um certificado — qualquer origem, inclusive um PKCS#12 enviado e o Azure
Key Vault, com a recusa por host do modo cluster inalterada — e ganha uma pasta do Lacuna Signer,
pelo id da pasta no Signer, no lugar de uma pasta de entrada. O vínculo é um para um e com a própria
pasta: dois perfis não podem compartilhar uma pasta, e um documento em uma subpasta não é do perfil. O
titular do certificado é identificado pelo CPF que o certificado traz — certificados e-CPF e e-CNPJ
trazem um —, nunca por um campo digitado. O perfil reaproveita o Signer:Endpoint e a Signer:ApiKey
deste host (um tenant por host), e é recusado em um host sem eles. O seed pode declarar um perfil assim —
o formato da configuração e todas as recusas estão em
Configuração —, e o
dashboard também, como descrito abaixo.
Escolhendo a pasta pelo dashboard
Em /profiles/_new, ou no painel Certificado de /profiles/{name}, escolha Pasta do Lacuna
Signer como método. Os campos do certificado continuam — este host assina com aquela chave — e um
seletor de Pasta do Lacuna Signer aparece acima deles: ele lista as pastas da organização do Signer a
que pertence a chave de API deste host, com busca pelo nome, inclusive pastas aninhadas, e grava o id
da pasta junto com o nome com que você a escolheu, que é o que a página do perfil mostra depois.
Apenas a própria pasta é vinculada, nunca as subpastas. Se não for possível contatar o Signer, o seletor
avisa e mantém a pasta já escolhida; nada mais no formulário é afetado. A página também informa, ao lado
do seletor, que o Signer escolhe o formato e que a política ICP-Brasil em um PDF é uma configuração da
organização no Signer.
Nesse método, o painel Comportamento esconde o que o Signer decide — a pasta de entrada, a verificação, a criptografia e a checagem CNAB240 — e as salva desligadas; o formato continua visível, mas é ignorado, guardado para uma mudança futura para um método que o leia. O salvamento é recusado, no seu idioma, quando há:
- nenhuma pasta, ou uma pasta em que outro perfil já assina — com o nome desse perfil. Dois salvamentos que escolhem a mesma pasta no mesmo instante são decididos pelo banco operacional, e o perdedor recebe a mesma mensagem;
- um host sem configurações
Signer:*; - o perfil
default— é para ele que vão os uploads sem perfil e as pastas sem vínculo, e este método não recebe arquivos; - a mudança de um perfil para o método enquanto ele ainda tem verificação, criptografia, validação do certificado ou a checagem CNAB240 ligadas, uma pasta de entrada ou uma regra de aprovação — desligue, limpe a pasta ou remova a regra nos respectivos painéis antes;
- a mudança de um perfil para fora do método quando ele não tem formato escolhido — escolha um no painel Comportamento antes;
- em
/profiles/_new, que abre o certificado enquanto você espera, um certificado sem CPF: nenhuma ação do fluxo no Signer poderia ser associada a ele. Use um e-CPF ou e-CNPJ ICP-Brasil.
O Signer escolhe o formato, não o perfil
Um PDF é assinado em PAdES, um documento XML em XAdES, e qualquer outro em CAdES (AD-RB) — o
Format do perfil é ignorado, e o job registra o formato que o Signer vai usar. Em um PDF, a aplicação
da política ICP-Brasil depende da configuração UseBrazilianPdfSigningPolicies da organização no
Signer, e não do Bulk Signer: o padrão ADR-Básica do produto não alcança uma assinatura que o Signer
monta. Verificação, criptografia, validação do certificado e a checagem CNAB240 são do Signer ou não se
aplicam; por isso o perfil precisa ter Verify, Encrypt, ValidateCertificate e CheckCnab240
desligados, e não tem regra de aprovação — o fluxo, aprovadores inclusive, é decidido no Signer.
Como um documento é encontrado
A cada Signer:FolderSweepIntervalSeconds (padrão: cinco minutos), uma varredura lista os documentos
pendentes da pasta de cada perfil habilitado e relê cada um com a própria chave de API deste host. Um
documento vira um job quando ainda está exatamente naquela pasta, ainda está pendente, e a ação do
fluxo pendente atual é uma ação de assinatura cujo identificador do participante é o CPF do titular.
Um documento cujos participantes anteriores ainda não agiram não gera nada — ele é encontrado de novo
quando chega a vez do titular, já que é o Signer, e não o Bulk Signer, que controla essa espera.
Cada par (documento, ação do fluxo) é definitivo depois de ter tido um job: um documento encontrado de novo — pela próxima varredura, ou por outra instância no modo cluster — não cria nada, qualquer que seja o status do job existente, e Tentar novamente é o único caminho de volta (abaixo). O par fica registrado como descoberto em um registro próprio, que excluir o job e o Limpar Jobs mantêm; assim, nenhum dos dois faz a varredura assinar o documento de novo.
Um documento pendente na pasta sem nenhuma ação de assinatura para o titular — nem mais adiante no
fluxo, nem já concluída — é algo que uma pasta vinculada um para um não deveria conter: ele é registrado
uma vez como um job Failed com signer-folder.no-holder-action, para que alguém o veja, e todas as
varreduras seguintes o deixam em paz.
A varredura roda com o pipeline pausado ou não — a descoberta só enfileira jobs; a pausa impede que eles sejam reivindicados — e a pasta de um perfil desabilitado é ignorada. Uma notificação do Signer reduz a espera para segundos — abaixo —, e a varredura continua sendo a garantia de qualquer forma.
Como ele é assinado
O job é reivindicado como qualquer outro — sob a chave de pausa e o Pipeline:MaxConcurrency, e é por
isso que cada assinatura é um job — e o worker:
- relê o documento e confere se a ação do fluxo ainda é do titular e ainda é a atual;
- pede ao Signer a URL de ação do titular (CPF e o e-mail do participante da ação, e o id da ação quando o titular tem mais de uma pendente) e extrai dela a chave do documento e o ticket;
- inicia uma assinatura pública com o certificado, recebendo o hash a assinar e o algoritmo de digest com que ele foi calculado;
- recusa qualquer coisa que não seja SHA-256, SHA-384 ou SHA-512 — SHA-1 e um digest desconhecido ou ausente, pelo nome, e um hash com o tamanho errado — antes de tocar na chave;
- assina o hash como hash com a chave do perfil e devolve a assinatura ao Signer, que a incorpora ao documento.
O job passa por Processing, depois por Verifying enquanto o Signer confere a assinatura, e então
Completed quando o Signer a aceita. A página do job e a lista /jobs mostram o nome do documento e o
tipo MIME dele.
Quando ele não é assinado
Qualquer outro desfecho é um de dois, e o documento nunca é cancelado nem recusado no Signer em nenhum deles: este host não o criou, e ele continua pendente lá.
Canceled— o documento seguiu adiante antes de este host assiná-lo. Ele foi cancelado, recusado ou expirou no Signer, a ação do titular já foi concluída, ou o Signer respondeu à URL de ação ou ao início que a ação não está pendente (NoPendingActionFoundForEmailAndIdentifier,SignatureNotPending,FlowActionNotPending) e uma segunda leitura confirma — uma segunda leitura que ainda mostra a ação pendente significa que o Signer não associou o participante, e o job terminaFailedcomsigner-folder.signature-failed. Nada deu errado aqui, então o job não tem código; o histórico dizNo longer pending at Lacuna Signer: <status>.Uma ação que volta a esperar uma etapa anterior não "seguiu adiante" — é um fluxo editado por baixo deste host, e falha comsigner-folder.signature-failed, já que um par que teve job nunca é redescoberto.Failedcom um código — todo o resto.signer-folder.signature-refusedquando o Signer recusou a assinatura na resposta (os resultados da validação ficam no histórico do job);signer-folder.key-unavailablequando a chave do perfil não assinou;signer.unreachablequando não foi possível contatar o Signer ou ele não respondeu de forma útil — uma conexão recusada, um timeout, um5xx,408ou429, ou uma resposta que não vem da API do Signer, como a página de erro de um proxy;signer-folder.action-url-malformedquando a URL de ação do Signer não trazia chave ou ticket;signer-folder.profile-changedquando o perfil do job saiu do método;signer-folder.interruptedquando a execução que o assinava terminou no meio da assinatura (abaixo);signer-folder.signature-failedpara o resto — o documento saiu da pasta, a ação deixou de ser do titular, um digest sob o qual este host não assina —, com o motivo no histórico. Cada um está em Diagnóstico de problemas.
Uma falha depois de a assinatura ter sido entregue ao Signer — uma conclusão que deu timeout — informa no
job que o Signer pode tê-la aceitado; um Tentar novamente então relê o documento e termina Canceled
em vez de assinar duas vezes. Uma falha depois de a assinatura ter sido calculada, mas antes de ser
entregue, informa isso: o documento está intacto no Signer.
Quando a execução que o assinava termina
Uma reinicialização, ou uma instância que parou de responder no modo cluster: a recuperação na
inicialização, e a sobrevivente que assume o job, não podem olhar em output/
para um job assim; por isso elas perguntam ao Lacuna Signer — uma leitura do documento com a chave de
API deste host, nada mais — e encerram o job pela resposta:
| O job estava | O Lacuna Signer informa | O job termina |
|---|---|---|
Processing, a chave nunca foi acionada | — (não é perguntado) | De volta à fila: nada foi tentado, então isto é recuperação, não uma nova tentativa. A próxima reivindicação relê o documento, como toda reivindicação |
Verifying — a assinatura foi entregue ao Signer | a ação do fluxo do titular Completed | Completed, com o histórico dizendo que o Signer informa a ação como concluída. Nada é gravado em disco; não há o que gravar |
Processing, depois da chave — a assinatura nunca saiu deste host | o documento ou a ação do titular seguiu adiante | Canceled, No longer pending at Lacuna Signer: … — o mesmo desfecho da etapa de assinatura, já que a assinatura deste job nunca chegou ao Signer |
| qualquer um, depois da chave | a ação do titular ainda pendente, ou qualquer estado que este host não interpreta como um desfecho | Failed, signer-folder.interrupted, com o histórico dizendo se a assinatura tinha sido entregue e o que o Signer informou |
| qualquer um, depois da chave | — o Signer não está configurado neste host, não responde ou responde com erro | Failed, signer-folder.interrupted, com o histórico dizendo que não foi possível perguntar ao Signer, e por quê |
Um job que passou da chave nunca volta à fila, diga o Signer o que disser: isso seria uma segunda
assinatura que ninguém decidiu. Confira o documento no Lacuna Signer antes de usar Tentar novamente
em um job signer-folder.interrupted — a nova tentativa o relê e termina Canceled se a ação do titular
já estiver concluída, então ela nunca assina duas vezes, de qualquer forma.
A consulta é limitada para que o Signer não consiga segurar um boot: cada leitura é interrompida após dez
segundos, a recuperação inteira para de perguntar após trinta, e, depois que o Signer deixa de responder,
os jobs restantes daquela recuperação seguem o caminho alternativo sem perguntar — o host sobe de
qualquer forma. Um job cuja conclusão este host não conseguiu registrar depois de o Signer aceitar a
assinatura é exatamente a linha Verifying que a tabela conclui no próximo início.
Tentar novamente é o único caminho de volta
A partir de Failed e também de Canceled — inclusive o cancelamento de um job Queued pelo operador,
que é só local e nunca chega ao Signer. Uma nova tentativa é um job novo que aponta o antigo como pai,
para o mesmo documento e a mesma ação do fluxo, e ela recomeça do zero: a reivindicação relê o
documento, pede uma nova URL de ação e abre um novo início, cujo token é de uso único. Ela assina
enquanto a ação do titular ainda está pendente e termina Canceled quando não está mais. Só um job por
(documento, ação do fluxo) trabalha por vez; por isso, uma segunda tentativa enquanto a primeira está na
fila é recusada com job.already-processing. Um documento registrado com
signer-folder.no-holder-action não aceita nova tentativa — não há nada deste host a repetir, e, quando
o titular é adicionado no Signer, a próxima varredura encontra a nova ação sozinha.
Quando o próprio perfil não consegue trabalhar
Três condições param um perfil SignerFolder sem parar o host nem nenhum outro perfil, e cada uma é
informada como um certificado que não abre: na própria página do perfil e em /profiles como Este
perfil não consegue assinar, como degraded: true no GET /api/profiles, e como uma linha
signing-profile:<name> em /api/ready que fica vermelha, mas não faz a resposta falhar. Um job já
enfileirado para o perfil falha com profile.degraded, citando o motivo; a varredura e as notificações
não criam job novo para ele, e um Tentar novamente cria, mas falha da mesma forma ao ser
reivindicado.
- Este host não tem configurações
Signer:*. Todo salvamento e o seed recusam o método em um host assim; então este é um perfil armazenado cujo host perdeu oSigner:Endpointe aSigner:ApiKeydepois — ou um irmão do cluster configurado de outro jeito. Sem credencial, a varredura nem chega a iniciar, e nada na pasta seria encontrado; em vez de não fazer nada em silêncio, o perfil é degradado citando as duas chaves. Configure as chaves e reinicie, ou tire o perfil do método na página dele. - O certificado não tem CPF. Nada no Signer poderia ser associado ao titular. Isso é detectado onde quer que o perfil seja resolvido — na inicialização e na próxima atualização depois de um salvamento que passou o perfil para o método com um certificado assim —, nunca impedindo o boot. Dê ao perfil um e-CPF ou e-CNPJ ICP-Brasil (lido na próxima reinicialização, como toda mudança de certificado), ou tire o perfil do método.
- A pasta vinculada não existe mais no Signer — foi excluída lá. Nada na inicialização pergunta ao
Signer sobre uma pasta: é a varredura que descobre isso, e ela só pergunta ao Signer se a pasta
ainda existe quando a listagem de pendentes da pasta volta vazia, já que o Signer lista uma pasta
inexistente como vazia. Só a resposta do próprio Signer de que a pasta não existe (
FolderNotFound) degrada o perfil; uma indisponibilidade durante a pergunta não degrada — é uma falha daquela varredura, avisada e perguntada de novo na próxima. Vincule o perfil a outra pasta na página dele: a degradação fica com a pasta antiga, na hora, sem reinicialização. A varredura também pergunta de novo a cada passada; assim, uma resposta errada —Signer:ApiKeyouSigner:Endpointapontando por um instante para outra organização — se resolve sozinha quando o Signer encontra a pasta. Uma reinicialização esquece a degradação, e a primeira varredura depois dela a descobre de novo.
A varredura ignora a pasta de um perfil degradado — sem listagem, sem leitura, sem job, e uma notificação para ela é descartada — e avisa uma vez, no console e no log durável, em vez de a cada passada. Veja Diagnóstico de problemas.
Nenhum arquivo chega a um perfil SignerFolder
Um upload que o nomeia, um reescaneamento ou uma pasta monitorada cujos arquivos são roteados para ele,
ou uma nova tentativa de um job de arquivo cujo perfil passou depois para o método é recusado na entrada
com profile.takes-no-files, antes de qualquer coisa ser preparada.
Métricas e auditoria
| Métrica | Tipo | O que ela acompanha |
|---|---|---|
bulksigner_signer_folder_jobs_enqueued_total{profile} | Counter | Jobs criados para documentos encontrados à espera, e para novas tentativas deles. Um documento encontrado de novo não cria nada e não é contado. |
bulksigner_signer_folder_signatures_total{outcome} | Counter | Tentativas de assinatura: signed, refused (pelo Signer, na resposta), no-longer-pending (seguiu adiante antes de existir uma assinatura; terminou Canceled) ou failed. |
bulksigner_signer_folder_sweep_errors_total{op} | Counter | Chamadas ao Signer que a varredura não conseguiu concluir (list, folder — a pergunta se uma pasta cuja listagem voltou vazia ainda existe —, read, enqueue) e passadas que falharam por inteiro (pass). Uma varredura que falha não perde nada; a próxima pergunta de novo. |
bulksigner_signer_notifications_total{outcome} | Counter | Notificações recebidas — veja abaixo. |
Uma varredura que continua falhando é uma linha de prontidão: signer-folder-sweep em /api/ready
fica vermelha depois de três passadas seguidas com alguma falha cada — uma Signer:ApiKey revogada faz
todas falharem — e /api/ready/details diz desde quando e o que falhou por último, por etapa e tipo
(nunca a mensagem do Signer, que pode citar a chave de um documento). Ela nunca muda o veredito: um 503
não traria o Signer de volta e pararia todo o resto que este host faz. Veja
Diagnóstico de problemas.
Os eventos operacionais são SignerDocumentEnqueued, SignerDocumentWithoutHolderAction,
JobSignedAtSigner e JobNoLongerPendingAtSigner.
Notificações — o webhook
A varredura encontra um documento dentro de Signer:FolderSweepIntervalSeconds. Uma notificação o
encontra em segundos: o Signer chama este host quando um documento é criado, ou quando um participante
assina ou aprova — a etapa anterior à do titular é como se descobre que "chegou a vez do titular", já que
o Signer não tem evento para uma ação que passa a ficar pendente. Ela é opcional, e só vale a pena onde o
Signer consegue alcançar este host; um host sem entrada pública perde minutos, nunca documentos.
Uma notificação é uma campainha, não uma ordem. Nada no corpo dela é levado em conta como verdade. Este host lê quais documentos ela cita e em que pasta ela diz que cada um está, enfileira uma checagem para cada um que está na pasta vinculada a um perfil habilitado e responde — sem chamar o Signer dentro da requisição. A checagem então faz exatamente o que a varredura faz para um documento: relê o documento no Signer com a própria chave de API deste host, e só enfileira um job se ele ainda está exatamente naquela pasta, pendente, e é a vez do titular. Uma notificação forjada pode, no máximo, fazer este host perguntar ao Signer sobre um documento; uma repetida não cria nada, porque a checagem segue o caminho da varredura e a idempotência dela. Os documentos de um perfil desabilitado são confirmados e descartados.
Configurando.
- Gere um segredo aleatório longo — pelo menos 32 caracteres — e defina-o neste host como
Signer:WebhookSecret(a variável de ambienteSigner__WebhookSecret; nunca noappsettings.json). Reinicie. Sem segredo, a rota responde404. - No Lacuna Signer, abra a página de integrações da organização e adicione um webhook:
- URL: o endereço público deste host seguido de
/api/signer/notifications— por exemplo,https://bulksigner.example.com/api/signer/notifications. Atrás de um proxy reverso, o endereço que o proxy publica. - Autenticação: Bearer, com o segredo do passo 1 como token. Não
X-API-Key— esse header é a credencial REST do próprio produto, e uma notificação que chega com ele é recusada. - Eventos: o Signer entrega todo evento a todo webhook da organização; este host age sobre
DocumentsCreated,DocumentSignedeDocumentApprovede apenas confirma o recebimento dos demais.
- URL: o endereço público deste host seguido de
- Use a entrega de teste do Signer, ou crie um documento na pasta vinculada, e leia
/api/ready/details: a linhasigner-notificationsdiz quando a última notificação chegou àquela instância. No modo cluster, cada instância informa só o que chegou a ela.
O que ela responde. 401 para um token Bearer ausente ou errado (signer-notification.unauthorized);
202 quando um documento em uma pasta vinculada foi enfileirado para checagem, ou já estava esperando
por ela; 204 para toda outra entrega autenticada — um evento que não pode deixar um documento à espera,
uma pasta sem vínculo, a pasta de um perfil desabilitado, um corpo que este host não consegue ler —,
porque o Signer tenta de novo qualquer resposta que não seja 2xx, e uma nova tentativa não mudaria nada
disso; 503 só quando este host falhou (signer-notification.unavailable): o banco operacional não
está utilizável nesta instância, ou há checagens demais esperando. 429 é o limite de requisições da
própria rota (RateLimiting:SignerNotification), que o Signer também tenta de novo. As checagens ficam
em memória: uma reinicialização esquece as que ainda não foram feitas, e a próxima varredura encontra os
documentos delas. O contrato completo está em API REST.
Referências cruzadas de diagnóstico
Veja em Diagnóstico de problemas os passos de diagnóstico para:
- API do Signer inacessível / avalanche de erros 5xx
- Chave de API errada —
401em todas as chamadas - Documento travado em
Pendingalém deSigner:TimeoutHours - O operador cancelou, mas o participante ainda vê o documento
- O dashboard não mostra o painel do Lacuna Signer mesmo com um perfil que o utiliza
- Um job de pasta do Lacuna Signer termina
FailedouCanceled - Um perfil de pasta do Signer está degradado — sem configurações
Signer:*, sem CPF, ou com uma pasta que não existe mais - A linha de prontidão
signer-folder-sweepestá vermelha - Documentos só são encontrados no intervalo da varredura, e não em segundos
A seguir: Arquivos de pagamento CNAB240. Anterior: Criptografia.