Pular para o conteúdo

Catálogo de demonstração. Produtos, criadores, preços e avaliações são fictícios. Recursos gratuitos podem ser adquiridos de verdade; itens pagos não são cobrados nem simulados. Ver o que já funciona

SDK Java

Cliente oficial. Zero dependências de runtime, Java 17+, o mesmo .jar em todas as plataformas.

Instalação

Gradle

build.gradle.kts
repositories {
    mavenCentral()
}

dependencies {
    implementation("gg.lodestone:lodestone-licensing:1.0.0")
}

tasks.shadowJar {
    relocate("com.lodestone.licensing", "com.seupacote.libs.licensing")
    minimize()
}

Maven

pom.xml
<dependency>
  <groupId>gg.lodestone</groupId>
  <artifactId>lodestone-licensing</artifactId>
  <version>1.0.0</version>
</dependency>

Com maven-shade-plugin, relocate com.lodestone.licensing do mesmo jeito.

A relocação não é opcional

Sem ela, dois plugins que usem o SDK colocam classes com.lodestone.licensing.* no mesmo classloader do servidor. O que carregar primeiro vence, e o segundo roda contra uma versão que não é a dele — com NoSuchMethodError em produção alheia, meses depois, sem pista nenhuma da causa.

Por que o SDK não tem dependências

Ele roda no mesmo classloader de dezenas de outros plugins. Qualquer dependência aqui vira conflito de versão no servidor de alguém. O preço é um parser de JSON de ~300 linhas escrito à mão; vale, porque a alternativa (Gson sombreado) já derrubou servidor em todo lugar onde dois plugins sombrearam versões diferentes no mesmo pacote.

Ciclo de vida

MomentoChamadaObservação
onEnable()activateAsync()Devolve um future. NÃO espere por ele.
Em segundo planoautomáticoO SDK agenda a renovação a 75% da vida da autorização, com piso de 30s.
No caminho quenteisAuthorized()Leitura local, sem rede. Seguro a cada tick.
onDisable()close()Sempre. Idempotente, espera até 5s pelas tarefas em voo.

Construção

java
LodestoneLicensing licensing = LodestoneLicensing.builder()
    .productId("lsp_...")                  // obrigatório
    .licenseKey(chaveDoCliente)            // obrigatório
    .dataFolder(getDataFolder().toPath())  // onde o estado local fica
    .platform(PlatformSupport.detect().id())
    .serverVersion(PlatformSupport.serverVersion())
    .baseUrl("https://lodestone.gg/api/v1/licensing")  // padrão
    .declaredAddress("203.0.113.10", 25565)  // só se a política vincular
    .testMode(false)
    .autoRenew(true)                       // padrão
    .timeouts(Duration.ofSeconds(10), Duration.ofSeconds(15))
    .maxAttempts(3)
    .listener(ouvinte)
    .build();

autoRenew(false) tem um custo

Desligar significa assumir a responsabilidade de chamar validateAsync() periodicamente. Sem isso, uma revogação só chega ao servidor no próximo reinício — o que anula boa parte do valor de poder revogar.

Eventos

java
licensing.addListener(new LicenseListener() {
    @Override
    public void onAuthorized(LicenseResult r) {
        // Autorizado. Ligue os recursos pagos AQUI, nunca antes.
        // r.features() traz o que a política liberou.
    }

    @Override
    public void onDenied(LicenseResult r) {
        // Expirou, foi revogada, suspensa, ou a instalação foi desvinculada.
        // Desligue os recursos. r.code() diz exatamente o quê.
    }

    @Override
    public void onGrace(LicenseResult r) {
        // A plataforma ficou inalcançável, mas a autorização guardada ainda
        // vale. O plugin CONTINUA funcionando — essa é a razão de existir
        // deste callback.
    }

    @Override
    public void onOffline(LicenseResult r) {
        // A tolerância acabou sem conseguir renovar. Único caso em que uma
        // indisponibilidade nossa derruba o recurso — e só depois de horas.
    }
});

Os callbacks rodam na thread do SDK

Qualquer coisa que toque a API do servidor precisa ser agendada. Use PlatformSupport.dispatcher(this), que resolve isso em todas as plataformas — inclusive no Folia, onde não existe “a thread principal”.

Estado e resultado

LicenseStateAutorizado?Quando acontece
UNKNOWNnãoAinda não houve tentativa.
AUTHORIZEDsimDocumento válido e dentro do prazo.
GRACEsimExpirou, mas dentro da tolerância offline. O plugin DEVE seguir funcionando.
INVALIDnãoChave inválida, produto errado, ou assinatura não confere.
EXPIREDnãoLicença vencida.
REVOKEDnãoRevogada em definitivo.
SUSPENDEDnãoSuspensa temporariamente.
LIMIT_REACHEDnãoTeto de instalações simultâneas atingido.
BINDING_MISMATCHnãoEndereço declarado não bate com o registrado.
RELEASEDnãoInstalação desvinculada ou bloqueada.
OFFLINEnãoSem rede e sem autorização guardada no prazo.
ERRORnãoFalha inesperada. Veja `message()`.

isValid() é verdadeiro em AUTHORIZED e GRACE — o modo offline conta como autorizado, que é o ponto dele.

Cache offline

O SDK guarda a última autorização assinada em plugins/SeuPlugin/licensing-state.json, junto com o identificador de instalação. Quando a plataforma não responde, ele usa o documento guardado — depois de verificar a assinatura de novo.

Guardar isso não é risco: o documento já traz o prazo dentro da assinatura, então salvá-lo não estica nada. Quem editar o arquivo quebra a assinatura e o SDK recusa.

Assinatura inválida NÃO cai para offline

Se a assinatura não confere, o SDK recusa direto, sem tentar o cache. Assinatura inválida significa resposta forjada, DNS sequestrado ou proxy injetando conteúdo — e, nesse cenário, o cache também seria suspeito.

Identificador de instalação

Gerado na primeira execução e persistido. É o que faz o servidor continuar sendo “o mesmo servidor” depois de reiniciar, de trocar de IP ou de mudar de máquina na hospedagem.

Não é derivado de hardware, e isso é deliberado: derivar de hardware parece mais seguro e não é — muda sozinho em hospedagem virtualizada, quebrando a licença de clientes legítimos, sem impedir ninguém de copiar o valor.

Apagar licensing-state.json gera uma instalação nova e consome uma vaga da licença. Por isso o arquivo é gravado de forma atômica e nunca reescrito à toa.

Tratamento de erros

Licença inválida não lança exceção: devolve um LicenseResult. É um resultado esperado do negócio, não um defeito — lançar obrigaria todo plugin a envolver a chamada em try/catch e trataria “o cliente não pagou” como exceção.

LicensingException é reservada para erro de programação: faltou oproductId, a chave está vazia, a JVM não tem Ed25519.

java
try {
    licensing = LodestoneLicensing.builder()
            .productId(PRODUCT_ID)
            .licenseKey(key)
            .build();
} catch (LicensingException e) {
    // Configuração inválida. Avise e desabilite o plugin.
    getLogger().severe(e.getMessage());
    getServer().getPluginManager().disablePlugin(this);
    return;
}

Boas práticas

  • Mensagem acionável por código. O administrador precisa saber o que fazer, não apenas que algo falhou. “Libere uma instalação em /dashboard/licenses e reinicie” resolve; “licença inválida” vira chamado.
  • Não desligue no primeiro erro de rede. Deixe o grace period trabalhar. O SDK já distingue indisponibilidade de recusa.
  • Não coloque a chave no código. Ela pertence ao administrador do servidor e vive no config.yml.
  • Não tente esconder a verificação. Ofuscação não impede quem controla a máquina e torna o seu plugin impossível de depurar quando alguém relatar um problema.