SDK Java
Cliente oficial. Zero dependências de runtime, Java 17+, o mesmo .jar em todas as plataformas.
Instalação
Gradle
repositories {
mavenCentral()
}
dependencies {
implementation("gg.lodestone:lodestone-licensing:1.0.0")
}
tasks.shadowJar {
relocate("com.lodestone.licensing", "com.seupacote.libs.licensing")
minimize()
}Maven
<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
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
Ciclo de vida
| Momento | Chamada | Observação |
|---|---|---|
onEnable() | activateAsync() | Devolve um future. NÃO espere por ele. |
| Em segundo plano | automático | O SDK agenda a renovação a 75% da vida da autorização, com piso de 30s. |
| No caminho quente | isAuthorized() | Leitura local, sem rede. Seguro a cada tick. |
onDisable() | close() | Sempre. Idempotente, espera até 5s pelas tarefas em voo. |
Construção
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
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
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
PlatformSupport.dispatcher(this), que resolve isso em todas as plataformas — inclusive no Folia, onde não existe “a thread principal”.Estado e resultado
| LicenseState | Autorizado? | Quando acontece |
|---|---|---|
| UNKNOWN | não | Ainda não houve tentativa. |
| AUTHORIZED | sim | Documento válido e dentro do prazo. |
| GRACE | sim | Expirou, mas dentro da tolerância offline. O plugin DEVE seguir funcionando. |
| INVALID | não | Chave inválida, produto errado, ou assinatura não confere. |
| EXPIRED | não | Licença vencida. |
| REVOKED | não | Revogada em definitivo. |
| SUSPENDED | não | Suspensa temporariamente. |
| LIMIT_REACHED | não | Teto de instalações simultâneas atingido. |
| BINDING_MISMATCH | não | Endereço declarado não bate com o registrado. |
| RELEASED | não | Instalação desvinculada ou bloqueada. |
| OFFLINE | não | Sem rede e sem autorização guardada no prazo. |
| ERROR | não | Falha 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
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.
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.
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.