Una licencia, muchos relojes: caducidad por derecho con Keyright
Una licencia suele tener una única fecha de caducidad, pero los contratos reales rara vez la tienen: un plan base se renueva anualmente mientras un complemento de prueba dura 30 días y una característica promocional termina con el trimestre. Fijar una sola fecha para toda la licencia te obliga a conceder de más o a reemitir constantemente. Aquí explicamos cómo dar a cada derecho su propia caducidad dentro de una única licencia Keyright firmada: la forma del payload, el SDK que lee un reloj por característica sin conexión, cómo aplicarlo en el límite de la capacidad y las reglas de gracia que evitan que un complemento caducado tumbe toda la aplicación.
La mayoría de las pilas de licenciamiento dan a una licencia exactamente un reloj: un expiresAt para toda la clave. Eso modela el contrato más simple (una suscripción anual que se renueva o caduca) y nada más. Los acuerdos comerciales reales son más enrevesados. Un cliente está en un plan base anual, probando un complemento premium durante 30 días y, además, lleva una característica promocional que le regalaste hasta el final del trimestre. Tres capacidades, tres fechas de fin distintas, un solo cliente.
Con una licencia de un único reloj tienes dos malas opciones. O bien emites varias licencias (una por fecha de fin) y ahora tu aplicación tiene que hacer malabares con un montón de claves y reconciliarlas, o bien eliges la fecha más tardía para toda la licencia y concedes de más, en silencio, la prueba y la promoción hasta entonces. Ninguna de las dos es honesta. La solución es dejar de pensar en la caducidad como una propiedad de la licencia y empezar a tratarla como una propiedad de cada derecho.
Un derecho puede llevar su propio reloj
Si has leído Derechos e indicadores de características, impulsados por la licencia, ya sabes que un derecho es una capacidad con nombre (un indicador booleano como export, o un límite numérico como max-projects) incorporada a la licencia firmada a partir de una plantilla de nivel. La caducidad por derecho añade un campo: cada derecho puede especificar cuándo deja de aplicarse.
El payload de la licencia sigue siendo un único objeto firmado; el bloque de derechos simplemente se vuelve más rico:
{
"sub": "acct_8f21c",
"tier": "pro",
"iat": "2026-10-12T00:00:00Z",
"exp": "2027-10-12T00:00:00Z",
"entitlements": {
"export": { "value": true, "exp": "2027-10-12T00:00:00Z" },
"sso": { "value": true },
"premium-trial": { "value": true, "exp": "2026-11-11T00:00:00Z" },
"promo-reports": { "value": true, "exp": "2026-12-31T23:59:59Z" },
"max-projects": { "value": 25, "exp": "2027-10-12T00:00:00Z" }
}
}
Ahora hay tres relojes visibles. El exp de nivel superior es la vida propia de la licencia: el límite exterior estricto. premium-trial muere el 11 de noviembre, promo-reports en Nochevieja, y sso no tiene ningún exp, lo que significa que vive exactamente tanto como la licencia. Cada derecho se evalúa contra su propia fecha, acotada por la de la licencia.
Leer un reloj por característica en tu aplicación
La superficie del SDK apenas cambia, y ese es el objetivo: IsEnabled y GetLimit ya devuelven la respuesta efectiva, y la pregunta “¿se ha agotado el reloj propio de este derecho?” simplemente se integra en esa respuesta. Un derecho caducado se reporta como deshabilitado:
var client = KeyrightClient.Initialize(options);
await client.ActivateAsync(licenseKey); // once; the lease is cached and verified
// premium-trial is true in the payload, but if its exp has passed,
// IsEnabled returns false — no special-casing at the call site.
if (client.IsEnabled("premium-trial"))
ShowPremiumPanel();
// A numeric limit whose clock has run out falls back to your conservative value.
int maxProjects = client.GetLimit("max-projects", fallback: 1);
Como toda la decisión es local (comparar el exp del derecho con la hora actual, dentro de un payload cuya firma ya has verificado contra la clave pública incrustada), no hay ningún viaje de ida y vuelta por la red ni nada nuevo que pueda fallar. La evaluación que realiza IsEnabled es aproximadamente:
internal bool IsEnabled(string key)
{
if (!_entitlements.TryGetValue(key, out var e)) return false; // unknown → closed
if (e.Value is not true) return false; // flag is off
if (e.Expiry is DateTimeOffset exp && _clock.UtcNow >= exp)
return false; // this feature's clock ran out
return true;
}
Todo aquello en lo que tu código ya confiaba sigue siendo cierto: los indicadores desconocidos fallan de forma cerrada, la firma se verifica sin conexión y manipular una marca de tiempo invalida toda la licencia. La única adición es la comparación _clock.UtcNow >= exp, acotada a un único derecho.
Cuando necesites la fecha en sí (para mostrar “Tu prueba premium termina en 6 días”), pídela directamente en lugar de escarbar en el payload:
DateTimeOffset? ends = client.ExpiresAt("premium-trial");
if (ends is { } d && d - DateTimeOffset.UtcNow < TimeSpan.FromDays(7))
ShowTrialEndingBanner(d);
ExpiresAt devuelve null para un derecho sin reloj (vive tanto como la licencia) y la fecha efectiva en caso contrario; nunca una fecha posterior al propio exp de la licencia, porque la licencia sigue siendo el límite exterior.
Aplicar la restricción en el límite de la capacidad
La regla general de los derechos se aplica sin cambios: ocultar un botón es UX, no aplicación de la licencia. Coloca la comprobación donde realmente vive la capacidad, y deja que el caso de la prueba caducada salga de la misma llamada a IsEnabled:
public Report BuildPromoReport(ReportOptions options)
{
if (!_license.IsEnabled("promo-reports"))
throw new FeatureNotLicensedException("promo-reports");
return _renderer.Render(options);
}
El 30 de diciembre esto se ejecuta. El 1 de enero el mismo código lanza una excepción: no porque hayas publicado nada, ni porque la licencia se haya vuelto inválida, sino porque el reloj de ese único derecho se agotó. El cliente conserva export, sso y todo lo demás de la licencia hasta el propio exp de la licencia.
Gracia, desfase y el reloj fiable
La caducidad por característica hereda los mismos dos peligros que tiene toda comprobación basada en el tiempo, y los gestionas de la misma manera.
Desfase y retroceso del reloj. Una comprobación sin conexión es tan honesta como el reloj del dispositivo. Un usuario que atrasa su reloj un mes resucita una prueba muerta, a menos que lo detectes. La detección de manipulación del reloj de Keyright (tratada en Manipulación del reloj de licencia y retroceso de tiempo) persiste una marca de agua monótona de la última hora que vio la aplicación, y se niega a aceptar un reloj de pared que haya retrocedido más allá de ella. La caducidad por derecho se apoya directamente sobre eso: el _clock.UtcNow de la evaluación anterior es la hora validada, no el DateTimeOffset.UtcNow en bruto.
Gracia, decidida por capacidad. Cuando la licencia entra en su ventana de gracia, normalmente mantienes la aplicación en modo de solo lectura durante unos días (consulta Caída del servidor de licencias y ventanas de gracia). Un único derecho que caduca es más benigno: el comportamiento predeterminado es un apagado inmediato en su exp, porque el resto de la aplicación no se ve afectado y el cliente solo pierde el complemento. Si quieres un aterrizaje suave para una característica concreta (digamos, unos días en los que una prueba se degrada a solo lectura antes de desaparecer), codifícalo como política allí donde lees el indicador, no como una segunda fecha de caducidad:
public bool CanEditWithPremium()
{
if (_license.IsEnabled("premium-trial")) return true;
// Optional soft landing: 3 read-only days after the trial clock lapses.
var ended = _license.ExpiredAt("premium-trial"); // when it lapsed, or null
return ended is { } t && DateTimeOffset.UtcNow - t < TimeSpan.FromDays(3)
? false // view allowed elsewhere; editing is off
: false;
}
Mantener la gracia en tu código y no en el payload significa que la licencia dice una sola cosa honesta (esta característica termina en esta fecha) y tu producto decide con cuánta elegancia soltarla.
Por qué esto es mejor que hacer malabares con claves
La caducidad por derecho elimina toda una categoría de dolores operativos:
- Una clave por cliente, siempre. Un complemento de prueba, una promoción y un plan base con tres fechas de fin distintas siguen entregándose como una única licencia. Tu flujo de activación, tu almacenamiento y tu equipo de soporte tratan con una sola clave.
- Las promociones y las pruebas caducan solas. Emites la licencia una vez con la fecha de fin de la promoción en ese único derecho. Nadie tiene que acordarse de reemitir o revocar cuando cambia el trimestre: el reloj ya está en el payload firmado.
- Venta adicional sin precipicio. Cuando la prueba premium caduca, la aplicación baja a la experiencia base por la que el cliente pagó, no a una pantalla de bloqueo. Ese es el momento de mostrar un aviso de actualización, y
ExpiresAtte da la fecha exacta alrededor de la cual construirlo. - Sigue siendo sin conexión y a prueba de manipulación. Cada reloj viaja dentro de la misma plantilla firmada, verificada localmente. Sin nuevos endpoints, sin nuevos viajes de ida y vuelta, y cualquier edición de cualquier fecha rompe la firma.
Una licencia nunca fue realmente un solo contrato con una sola fecha de fin; simplemente era lo más fácil de modelar. Da a cada derecho su propio reloj y la licencia por fin coincidirá con el acuerdo que realmente vendiste. Consulta la guía de derechos de Keyright para conocer el modelo de capacidades sobre el que se construye esto, y manipulación del reloj de licencia para el reloj validado del que depende.
Prueba Nebula.NET
Endurece tu código .NET en minutos — empieza con la edición gratuita.