rymga ← Volver a Licenses

SDK y ejemplos

Hay un SDK oficial: Java, sin dependencias — y es una implementación de referencia del contrato de validación. Cualquier otro lenguaje se comunica directamente con el mismo endpoint: construye la solicitud, verifica una firma RSA y lee el JSON. A continuación tienes el código completo, listo para ejecutarse.

El SDK de Java

Descarga el JAR del SDK desde tu panel (JDK 17+, sin dependencias transitivas) y añádelo al classpath. Para usarlo como dependencia de Maven o Gradle, instálalo primero en tu repositorio local:

mvn install:install-file \ -Dfile=rymga-licenses-sdk-1.0.0.jar \ -DgroupId=com.rymga -DartifactId=rymga-licenses-sdk \ -Dversion=1.0.0 -Dpackaging=jar
<!-- then declare it in your pom.xml --> <dependency> <groupId>com.rymga</groupId> <artifactId>rymga-licenses-sdk</artifactId> <version>1.0.0</version> </dependency>

Configúralo una vez con tu tenant, el código de producto y la clave pública integrada; después valida:

// PUBLIC_KEY = your tenant public key (X.509 SubjectPublicKeyInfo, Base64), pinned as a constant. LicenseClient client = LicenseClient.builder() .baseUrl("https://licenses.rymga.com") .tenantSlug("acme") .productCode("MY_APP") .publicKeyBase64(PUBLIC_KEY) .build(); // Stable per-machine id, persisted in your data folder (survives restarts/containers). String serverId = PersistentServerId.getOrCreate(dataFolder.toPath()); LicenseResult r = client.validate(licenseKey, serverId); if (!r.valid()) { // granted AND signature verified if (r.networkError()) { /* your policy: block, or a grace window */ } return lockOut(r.reason()); } String tier = r.tier(); // e.g. "PREMIUM" Object seats = r.feature("seats"); // read a feature flag

El serverId determina el server cap, por lo que debe ser estable para cada máquina — un identificador que cambia en cada reinicio hace que una máquina consuma varias plazas y acabe alcanzando SERVER_CAP_EXCEEDED. PersistentServerId.getOrCreate(dir) escribe una vez un identificador aleatorio en <dir>/.rymga-server-id y lo reutiliza; no le afectan los cambios del nombre de host ni de la dirección MAC. Indica una carpeta que se conserve entre reinicios. ServerFingerprint.derive() es la alternativa aproximada —identificador de máquina del sistema operativo y direcciones MAC, sin incluir el nombre de host, que puede cambiar—.

El objeto de resultado

El SDK devuelve un LicenseResult. Estos son los miembros importantes:

MiembroSignificado
valid()Solo es true si el backend concedió la licencia y la firma se verificó.
signatureValid()Indica por separado si la firma de la respuesta es válida.
networkError()Es true cuando la llamada no pudo llegar al backend; sirve para gestionar el periodo de gracia.
grace()Es true cuando el veredicto procede de la caché del periodo de gracia sin conexión (consulta más abajo).
reason()Motivo de denegación cuando no es válido (consulta la página de la API de validación).
tier()Plan al que pertenece la licencia, por ejemplo PREMIUM.
expiresAt()Fecha de caducidad Instant, o null para una licencia perpetua.
featureFlags() / feature(k)Mapa de funciones o una sola función por clave.
rawBody()Bytes exactos de la respuesta, si los necesitas.

Periodo de gracia sin conexión (opcional). Para que un fallo puntual al contactar con el backend no interrumpa software que tu cliente ya ha pagado, activa .offlineGrace(Duration.ofHours(72)) en el constructor. Cuando no se puede acceder al backend, validate() reutiliza la última respuesta favorable cuya firma se haya verificado, siempre que pertenezca a ese intervalo, y establece grace() == true. Es opcional y está limitado deliberadamente: durante el intervalo, una licencia revocada seguirá funcionando hasta que el cliente vuelva a contactar con el backend, que entonces elimina la concesión de la caché. El periodo de gracia nunca reactiva una licencia después de su expiresAt. Mantén el TTL tan corto como permitan tus necesidades de disponibilidad.

Cualquier otro lenguaje

No necesitas un SDK: el contrato es pequeño. Estos ejemplos son completos: construyen la solicitud, verifican la firma y leen el veredicto. Cópialos, configura tu clave pública y serverId; funcionarán directamente.

import crypto from "node:crypto";        // Node 18+, no dependencies

const url = "https://licenses.rymga.com/t/acme/api/v1/validate";
const res = await fetch(url, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    licenseKey, productCode: "MY_APP", serverId,
    nonce: crypto.randomUUID(), timestamp: Date.now(),
  }),
});

const body = Buffer.from(await res.arrayBuffer());        // exact bytes
const ok = crypto.createVerify("RSA-SHA256")
  .update(body)
  .verify(PUBLIC_KEY_PEM, res.headers.get("X-Signature"), "base64");
if (!ok) throw new Error("SIGNATURE_INVALID");

const verdict = JSON.parse(body);
if (!verdict.valid) lockOut(verdict.reason);
else if (verdict.tier === "PREMIUM") enablePremium();
# pip install cryptography   (stdlib has no RSA verifier)
import json, time, uuid, base64, urllib.request
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding

url = "https://licenses.rymga.com/t/acme/api/v1/validate"
body = json.dumps({
    "licenseKey": license_key, "productCode": "MY_APP", "serverId": server_id,
    "nonce": str(uuid.uuid4()), "timestamp": int(time.time() * 1000),
}).encode()

res = urllib.request.urlopen(urllib.request.Request(
    url, data=body, headers={"Content-Type": "application/json"}))
raw = res.read()                                          # exact bytes
sig = base64.b64decode(res.headers["X-Signature"])

pub = serialization.load_pem_public_key(PUBLIC_KEY_PEM)
pub.verify(sig, raw, padding.PKCS1v15(), hashes.SHA256())  # raises on failure

verdict = json.loads(raw)
if not verdict["valid"]:
    lock_out(verdict["reason"])
C#, Go y otros lenguajes: los mismos tres pasos; la verificación RSA está en sus bibliotecas estándar (RSA.VerifyData / rsa.VerifyPKCS1v15). La página Firma incluye el fragmento de verificación para cada uno.

Funcionamiento sin conexión y periodo de gracia

Tú decides qué hacer cuando no se puede acceder al backend — el SDK de Java expone networkError(), mientras que en un cliente directo la solicitud lanza un error:

  • Denegar ante cualquier fallo — bloquea hasta que una comprobación tenga éxito. Es la opción más estricta, pero perjudica a quienes trabajan sin conexión.
  • Periodo de gracia — recuerda el último veredicto firmado correcto y su fecha, y sigue funcionando, por ejemplo, 72 horas antes de exigir una nueva comprobación.

Elijas lo que elijas, guarda en caché únicamente veredictos cuya firma hayas verificado; nunca un valid: true.

Refuerzo

  • Valida periódicamente, no solo al inicio: repite la validación cada pocas horas durante la ejecución.
  • Fija la clave pública como constante en la compilación; nunca la obtengas en tiempo de ejecución mediante HTTP sin cifrar.
  • No confíes nunca en valid: true sin una firma verificada.
  • Ofusca la comprobación para que sea difícil localizarla y eliminarla. En compilaciones para la JVM, Lock Master cifra las cadenas y el flujo de control alrededor del código de validación, lo que dificulta mucho anular la comprobación mediante un parche.
  • No bases todo en un único booleano. Reparte las consecuencias de una licencia no válida para que modificar un único if no lo desbloquee todo.