Skip to content

Licenciamento de Módulos

Os módulos comerciais (Núcleos, Voluntários, etc.) são embarcados no bundle do Worker e ativados por uma licença — um JWT assinado com Ed25519 contendo os módulos que o cliente contratou. A verificação é 100% offline, no boot do Worker: sem chamadas de rede, sem serviço externo, sem phone-home.

Este guia cobre o ciclo de vida completo da licença, do ponto de vista do operador:

  1. Modelo — o que é uma licença e como ela é aplicada
  2. Gerar o par de chaves — uma única vez
  3. Emitir uma licençapnpm sign-license
  4. ImplantarLICENSE no Worker
  5. Rotação de chave — trocar a chave de assinatura

Como funciona

Máquina de assinatura (privada):
  license-private.pem ──pnpm sign-license──> LICENSE JWT ──envio──> cliente

Worker (boot):
  env.LICENSE ──verifica──> chave pública embutida ──ok──> rotas do módulo registradas
                                    └──falha──> módulo desativado (rotas 404)

Formato da licença

A licença é um JWT compacto (alg: "EdDSA") com os seguintes claims:

ClaimDescriçãoObrigatório
subIdentificador do cliente (ex.: igreja-batista)sim
modulesLista de módulos contratados (ex.: ["nucleus"])sim
planNome do plano (opcional, ex.: growth)não
expExpiração em epoch seconds — ausente = sem expiraçãonão

Aplicação (enforcement)

  • A licença é lida de env.LICENSE e verificada no boot por loadLicense() (assinatura Ed25519 + shape dos claims).
  • Uma rota de módulo só é registrada se o módulo estiver licenciado E habilitado (AND-gate):
    • licenciado: isModuleLicensed(id) — assinatura válida, id em modules, e dentro do período de graça (ver abaixo);
    • habilitado: ENABLED_MODULES (lista separada por vírgula ou *; default *).
  • Fail-closed: sem LICENSE (ou com licença inválida/expirada), as rotas do módulo simplesmente não existem — retornam 404. O core (presença, alunos, turmas, relatórios) nunca é afetado.

Período de graça

Após exp, o módulo continua ativo por 30 dias — o cliente em renovação não sofre corte abrupto. Depois do período de graça, as rotas do módulo são desativadas. Uma licença sem exp (emitida com --no-expiry) nunca expira.

Gerar o par de chaves

Gere o par uma única vez, na máquina de assinatura (nunca no CI, nunca no repo):

bash
openssl genpkey -algorithm Ed25519 -out license-private.pem
openssl pkey -in license-private.pem -pubout -out license-public.pem
  • license-private.pem — NUNCA commitar, nunca enviar. Guarde em um cofre de segredos / gerenciador de senhas. Quem tem essa chave consegue emitir licenças.
  • license-public.pem — a chave pública é embutida no build do Worker, em workers/src/license/key.ts (constante LICENSE_PUBLIC_KEY, única e trocável — é o ponto de rotação). Não é uma env var: não pode ser trocada em runtime.

O repo mantém uma chave de dev em workers/.dev-license-private.pem (gitignored) usada pelos testes de integração e por licenças de desenvolvimento. Antes do primeiro cliente pagante, gere um par de produção (comandos acima) e substitua a constante em key.ts.

Emitir uma licença

Use o CLI pnpm sign-license (raiz do repo):

bash
# via env var (PEM) ou --key-file
LICENSE_PRIVATE_KEY="$(cat license-private.pem)" pnpm sign-license \
  "igreja-batista" "nucleus,volunteers" --plan growth --expiry-days 365

# ou, para uma licença que nunca expira:
LICENSE_PRIVATE_KEY="$(cat license-private.pem)" pnpm sign-license \
  --no-expiry "igreja-batista" "nucleus"

Opções

OpçãoDescriçãoDefault
customer-idIdentificador do cliente (sub)
modulesLista separada por vírgula (ex.: nucleus,volunteers)
--expiry-days NDuração em dias365
--no-expiryOmite exp — licença sem expiraçãooff
--plan NAMENome do plano (plan)
--key-file PATHCaminho para a chave privada PEM (PKCS#8)
--helpMostra a ajuda

A chave privada é lida de --key-file ou da env var LICENSE_PRIVATE_KEY (PEM). Sem nenhuma das duas, o CLI falha com exit 1.

Saída

  • O JWT vai para o stdout (use para copiar/colar).
  • Um resumo legível vai para o stderr: license minted: customer=igreja-batista modules=[nucleus, volunteers] expiry=2027-07-31T00:00:00.000Z plan=growth

Envie o JWT por e-mail ao cliente (ou configure você mesmo na implantação — ver abaixo).

Implantar

Defina a env var LICENSE do Worker com o JWT emitido:

  1. workers/wrangler.toml[vars] (a variável já está documentada, comentada, apontando para este guia):

    toml
    [vars]
    LICENSE = "eyJhbGciOiJFZERTQSJ9..."
  2. Ou no Cloudflare dashboard → Workers → api-neemiasSettings → Variables → adicionar LICENSE (prefira Encrypt para a variável).

  3. (Opcional) restrinja quais módulos o deploy habilita:

    toml
    ENABLED_MODULES = "nucleus"   # default "*" — tudo que a licença permite
  4. Deploy:

    bash
    pnpm deploy:worker

Verificar

bash
# Rota de módulo ativa → 401 (existe, exige autenticação)
curl -i https://api.neemias.app/api/v1/nuclei

# Módulo desativado / sem licença → 404
curl -i https://api.neemias.app/api/v1/nuclei   # (com LICENSE ausente/inválida)

Se a rota retorna 401, o módulo está licenciado e registrado. Se retorna 404, a licença está ausente, inválida, expirada (além da graça) ou o módulo não está em ENABLED_MODULES. Observação: uma rota registrada sem exigência de autenticação retornaria 200 — o ponto é que as rotas de um módulo não licenciado retornam 404, nunca ficam acessíveis.

Rotação de chave

Trocar a chave de assinatura invalida todas as licenças existentes — planeje a janela e re-emita tudo:

  1. Gere um novo par: openssl genpkey -algorithm Ed25519 ... (comandos em Gerar o par de chaves).
  2. Substitua a constante LICENSE_PUBLIC_KEY em workers/src/license/key.ts pela nova chave pública.
  3. Atualize os tokens de teste (assinados com a chave antiga deixam de validar):
    • workers/vitest.integration.config.ts (binding LICENSE);
    • workers/src/__tests__/gating-fetch.test.ts (LICENSE_NUCLEUS e LICENSE_VOLUNTEERS_ONLY). Re-emita com: pnpm sign-license --no-expiry integration-tests nucleus,volunteers e pnpm sign-license --no-expiry test-volunteers-only volunteers.
  4. Deploy do Worker (a nova chave pública passa a valer).
  5. Re-emita a licença de cada cliente com a nova chave privada e envie.

Troubleshooting

SintomaCausa provávelAção
Rotas do módulo retornam 404LICENSE ausente / inválida / fora da graçaConfira a env var e re-emita a licença
401 em rota de móduloLicença OK — rota registrada, exige autenticaçãoNada a fazer (comportamento esperado)
ENABLED_MODULES restringe mas a licença cobre mais módulosConfiguração intencional de deployAjuste ENABLED_MODULES
Licença "válida" mas rota 404Módulo não está em modules da licençaRe-emita incluindo o módulo

Referências

  • Issue #466 — especificação do licenciamento (Ed25519 JWT, offline)
  • workers/src/license/validate.tsloadLicense() / isModuleLicensed()
  • workers/src/license/key.ts — chave pública embutida + rotação
  • Ativação de Módulos — visão do cliente

Distribuído sob licença MIT.