A camada de assinatura
O SignerAdapter é a costura de capacidade que decide de onde vem o poder de assinar, trocável sem mexer nos formatos de documento.
A ideia central: o SignerAdapter é a costura por onde o poder local de assinar entra. Ele responde onde os bytes são assinados — um .pfx A1 ou HSM — enquanto os formatos de documento (core, XML, PDF) conversam com um único formato fixo. APIs de provedores possuem cerimônias remotas separadas.
O contrato do SignerAdapter
O tipo abaixo é a definição literal exportada por @signature-kit/core/config. Seu doc-comment fixa a regra que esta página detalha: o SignerAdapter é dono de onde vem o poder de assinar e nunca da mutação de formato.
/**
* The capability seam. A signer owns "where the signing power comes from".
* It never owns document-format mutation (XML/PDF live in format modules).
*/
export type SignerAdapter = {
readonly id: string;
inspect(): Effect.Effect<SignerIdentity, SignatureKitError>;
certificate(): Effect.Effect<Certificate, SignatureKitError>;
importSigningKey(algorithm: SignatureAlgorithm): Effect.Effect<CryptoKey, SignatureKitError>;
sign(input: SignInput): Effect.Effect<SignatureArtifact, SignatureKitError>;
verify(input: VerifyInput): Effect.Effect<VerificationResult, SignatureKitError>;
};Os seis membros da costura
Toda implementação de SignerAdapter entrega exatamente estes seis membros:
id: string— identidade do backend de assinatura. O A1 usa"a1". APIs de provedores como Clicksign, Assinafy, ZapSign, DocuSeal e Documenso usam funçõescreate*SignatureRequest(...)e não passam por esta costura de mutação de XML/PDF.inspect()— retornaEffect<SignerIdentity, SignatureKitError>comsubject,issuer,serialNumber,thumbprint,validFrom,validToedocument?(CPF/CNPJ quando ICP-Brasil).certificate()— retornaEffect<Certificate, SignatureKitError>, o certificado completo (certPem,certificateDer,publicKeyDere aprivateKeyPem: Redacted<string>).importSigningKey(algorithm)— materializa aCryptoKeyde assinatura para oSignatureAlgorithmsolicitado ("rsa-sha256"ou"rsa-sha512").sign(input)— recebeSignInput({ content, algorithm }) e retorna umSignatureArtifact({ algorithm, signature }).verify(input)— recebeVerifyInput({ content, signature, algorithm }) e retornaVerificationResult, cujo campo évalid: boolean.
Todo membro falha pelo canal de erro tipado SignatureKitError — nunca por uma exceção solta. A flag retryable
é decidida por chamada, não fixada no código.
Construindo um A1
O pacote @signature-kit/a1 implementa a costura a partir de um arquivo .pfx/.p12. loadA1SignerAdapter(options) faz o parsing assíncrono e retorna um SignerAdapter pronto, com id "a1".
npm install @signature-kit/core @signature-kit/a1import { loadA1SignerAdapter } from "@signature-kit/a1/signer"
import { Effect, Redacted } from "effect"
// loadA1SignerAdapter -> Effect<SignerAdapter, SignatureKitError>
const signer = yield* loadA1SignerAdapter({
pfx, // Uint8Array — PKCS#12 bytes (start at 0x30)
password: Redacted.make(process.env.A1_PASSWORD ?? ""),
})
signer.id // "a1"
const identity = yield* signer.inspect() // SignerIdentity (e-CPF / e-CNPJ)
const artifact = yield* signer.sign({ content, algorithm: "rsa-sha256" })A1SignerOptions tem exatamente duas chaves — pfx: Uint8Array e password: Redacted.Redacted<string>. Os bytes
precisam ser PKCS#12 cru (começando em 0x30); um buffer vazio é rejeitado.
Disponibilizando como serviço
O core define Signatures como um Context.Service com a tag "@signature-kit/core/Signatures". Você nunca o implementa na mão: passe um SignerAdapter para signaturesLayer(signer) e receba de volta um Layer<Signatures> (via Layer.succeed). Os accessors signatures.{inspect, certificate, importSigningKey, sign, verify} declaram a dependência de Signatures; a layer a satisfaz.
import { signaturesLayer, signatures } from "@signature-kit/core/signatures"
import { loadA1SignerAdapter } from "@signature-kit/a1/signer"
import { Effect, Redacted } from "effect"
const program = Effect.gen(function* () {
// Signatures accessors — they require the service, they don't know the backend.
const identity = yield* signatures.inspect()
const artifact = yield* signatures.sign({ content, algorithm: "rsa-sha256" })
const result = yield* signatures.verify({
content,
signature: artifact.signature,
algorithm: artifact.algorithm,
})
return result.valid
})
const signer = yield* loadA1SignerAdapter({ pfx, password: Redacted.make(pwd) })
// signaturesLayer(signer) -> Layer<Signatures> (Layer.succeed)
yield* program.pipe(Effect.provide(signaturesLayer(signer)))Provedor automático para formatos
Para os formatos, o A1 oferece um atalho: a1SignaturesLayer(options) retorna um Layer<Signatures, SignatureKitError> diretamente, sem instanciar o adapter antes. signXml e signPdf exigem Signatures no canal de requisitos; basta entregar essa layer via Effect.provide.
npm install @signature-kit/xml @signature-kit/pdfimport { signXml } from "@signature-kit/xml/sign"
import { xmlRuntimeLayer } from "@signature-kit/xml/runtime"
import { signPdf } from "@signature-kit/pdf/sign"
import { a1SignaturesLayer } from "@signature-kit/a1/signer"
import { Effect, Redacted } from "effect"
// a1SignaturesLayer(options) -> Layer<Signatures, SignatureKitError>
const layer = a1SignaturesLayer({ pfx, password: Redacted.make(pwd) })
// The formats require Signatures; the layer is the only thing that changes per backend.
const signedXml = yield* signXml({ xml, referenceId: "nfe-1" })
.pipe(Effect.provide(layer), Effect.provide(xmlRuntimeLayer))
const signedPdf = yield* signPdf({ pdf, policy: "pades-icp-brasil" })
.pipe(Effect.provide(layer))verifyXml e verifyPdf não exigem Signatures — a verificação não precisa da costura, porque a chave pública vem
do próprio documento.
Trocando o backend
Como a costura é uma porta de formato fixo, trocar de onde vem o poder de assinar é trocar o argumento de signaturesLayer(...). O trabalho de assinatura é idêntico; nada do formato muda.
import { signPdf } from "@signature-kit/pdf/sign"
import { signaturesLayer } from "@signature-kit/core/signatures"
import type { SignerAdapter } from "@signature-kit/core/config"
import { Effect } from "effect"
// The seam is just a port: any SignerAdapter works, as long as it has an id.
declare const a1Signer: SignerAdapter // from loadA1SignerAdapter (id "a1")
declare const hsmSigner: SignerAdapter // another backend that satisfies the contract
const job = signPdf({ pdf, policy: "pades-ades" })
// Same job, two backends — nothing about the PDF changes.
const withA1 = yield* job.pipe(Effect.provide(signaturesLayer(a1Signer)))
const withHsm = yield* job.pipe(Effect.provide(signaturesLayer(hsmSigner)))Qualquer valor que satisfaça SignerAdapter funciona. Aqui o A1 vem de loadA1SignerAdapter; outro backend só precisa
entregar os mesmos seis membros e um id.
Onde a costura termina
A regra do doc-comment é deliberada: o SignerAdapter é dono de onde vem o poder de assinar e nunca da mutação do documento. Ele opera sobre content: Uint8Array e retorna uma signature: Uint8Array — bytes opacos. Embutir uma assinatura em um XML envelopado ou em um PDF PAdES é trabalho dos módulos de formato (@signature-kit/xml, @signature-kit/pdf), que consomem a costura via Effect.provide e por isso permanecem neutros ao backend.
Esse corte mantém a aplicação estável: o XML não sabe se a chave veio de um .pfx ou de um HSM, e o signer não sabe se os bytes que recebeu são uma NF-e ou um PDF. Cada lado conhece apenas a sua metade da fronteira.
Erros que você pode encontrar
Toda falha da costura é um SignatureKitError tipado. Os mais comuns ao construir e usar o signer: