Skip to content
← Todas las publicaciones
· Delta1 Labs Licenciamiento.NETGuía

Vender actualizaciones: cómo decide una licencia qué versiones puede ejecutar

Una licencia perpetua es para siempre; "un año de actualizaciones" no lo es, y ambas conviven en la misma clave. La cobertura de versiones es la forma en que una licencia declara qué versiones tiene derecho a ejecutar, como concesiones firmadas de rango de versión y de ventana temporal que la compilación comprueba localmente, sin conexión y sin reactivación. Aquí tienes cómo se da forma a una concesión de cobertura, cómo la fecha de derecho (no la de instalación) decide un parche, cómo el bloque firmado viaja junto al lease y cómo tu compilación lo exige sin una sola llamada de red en la ruta crítica.

Una licencia perpetua y una ventana de mantenimiento son promesas distintas que los clientes compran rutinariamente como un solo SKU. “Perpetua, con un año de actualizaciones” significa que la licencia es válida para siempre pero solo tiene derecho a las versiones publicadas en el primer año. Codifícalo como una caducidad y habrás roto la promesa de perpetuidad: la app deja de funcionar al cabo de un año. Ignóralo y habrás regalado cada versión futura por un pago único.

Keyright separa ambas cosas. La validez —firma, asiento, caducidad, revocación— responde a ¿es buena esta licencia? La cobertura de versiones responde a una pregunta distinta: ¿puede esta licencia ejecutar esta versión? La cobertura es un conjunto de concesiones firmadas adjuntas a una licencia, evaluadas localmente en tu compilación contra la versión que es, sin reactivación y sin red en la ruta crítica. Este artículo trata de cómo se da forma a esa maquinaria y cómo la conectas a una compilación .NET.

Una concesión es un rango de versión, una ventana temporal, o ambas

Una concesión de cobertura es la unidad más pequeña de “tienes derecho a estas versiones”. Puede restringir por versión, por fecha, o por ambas a la vez:

  • Rango de versión — [2.0.0, 3.0.0): toda versión 2.x, nada a partir de la 3.0. Esto es “una versión mayor”.
  • Ventana temporal — versiones publicadas entre dos fechas. Esto es “un año de actualizaciones”, expresado como las versiones cuya fecha de derecho cae dentro de la ventana.
  • Ambas — un rango y una ventana, intersectados: “versiones 2.x publicadas en tu primer año”.

Rara vez añades concesiones en crudo a mano. Un preset expande un término en lenguaje llano a la forma de concesión correcta —lifetime, updates-for-period, one-major, one-major-plus-upgrade-protection, subscription, subscription-with-fallback— de modo que “un año de actualizaciones desde la compra” se convierte en una concesión de ventana temporal anclada en la fecha de compra sin que calcules nada.

# Aplica un preset a una licencia; se expande en las concesiones concretas.
curl -X POST "$BASE/admin/licenses/$ID/coverage/preset" \
  -H "X-Admin-Token: $TOKEN" -H "content-type: application/json" \
  -d '{"preset":"updates-for-period","months":12,"from":"2026-10-10"}'

La fecha de derecho es todo el truco

La parte sutil de una ventana temporal es qué fecha mide una versión. No es la fecha de instalación del cliente, ni el reloj del sistema, ni la marca de tiempo del archivo. Cada versión que registras lleva una fecha de derecho —la fecha contra la que se juzga la cobertura— y una concesión de ventana temporal cubre una versión cuando esa fecha de derecho cae dentro de la ventana.

# Registra cada versión para que la cobertura tenga una fecha contra la que juzgarla.
curl -X POST "$BASE/admin/products/acme-app/releases" \
  -H "X-Admin-Token: $TOKEN" -H "content-type: application/json" \
  -d '{"version":"3.1.0","entitlementDate":"2026-09-01","channel":"stable"}'

Esto importa sobre todo para los parches. Si la ventana de actualizaciones de un cliente se cerró el 1 de agosto y publicas 3.1.4 en octubre para corregir un fallo de 3.1.0 (que estaba dentro de su ventana), juzgar 3.1.4 por su propia fecha de publicación la empujaría fuera de la ventana y le bloquearía una corrección a la que claramente tiene derecho. En su lugar, un parche hereda la fecha de derecho de su versión base, así que 3.1.4 se juzga exactamente como se juzgó 3.1.0. La regla: el tiempo de calendario en la máquina del cliente nunca entra en juego; solo la fecha de derecho de la versión.

cobertura de licenciarango [2.0.0, 3.0.0)ventana ene–dic 2026ventana de fechas de derecho2.3.0feb · cubierta2.6.0jun · cubierta2.6.1 parchehereda fecha de 2.6.03.0.0fuera de rango

El bloque firmado viaja con el lease

La cobertura solo es fiable si la compilación no puede engañarse a sí misma sobre ella, así que las concesiones se firman en un bloque de cobertura con la misma clave del inquilino que firma la licencia, verificable sin conexión contra la clave pública compilada en tu binario. La activación entrega ese bloque y lo almacena en caché junto al lease, así que para cuando tu app pregunta “¿estoy cubierto?”, la respuesta ya está en disco. Sin descarga aparte, sin servidor de cobertura, sin red en la ruta que controla el arranque.

Exigirlo en una compilación .NET

El paquete Keyright.NET incluye targets de MSBuild que estampan la identidad de esta compilación en el ensamblado. Configúralos desde tu canalización de publicación para que el binario enviado sepa qué versión es y contra qué fecha debe juzgarse:

<PropertyGroup>
  <KeyrightCoverageVersion>3.1.0</KeyrightCoverageVersion>                 <!-- this build -->
  <KeyrightCoverageEntitlementDate>2026-09-01</KeyrightCoverageEntitlementDate>  <!-- base release's date for a hotfix -->
  <KeyrightCoverageChannel>stable</KeyrightCoverageChannel>               <!-- or beta -->
  <KeyrightRequireCoverage>true</KeyrightRequireCoverage>                 <!-- enforce; build fails (KR0001) if the date is missing -->
</PropertyGroup>

Al arrancar preguntas una vez. El SDK lee el bloque en caché, decide localmente, refresca en silencio si la respuesta en caché es “no” (la cobertura puede haberse ampliado desde la activación) y nunca lanza excepciones por problemas de red:

using Keyright.Coverage;

if (CoverageBuildInfo.Current.RequireCoverage)
{
    CoverageCheckResult check = await License.CheckCoverageAsync(ct: ct);
    if (!check.IsCovered)   // Covered and NotConfigured both pass
    {
        // check.Status: NotCovered  (Reason = WindowLapsed → renew,
        //                            OutsideVersionRange → upgrade entitlement)
        //            or UnavailableOffline (nothing cached, no network → import a coverage file)
        RunReadOnlyOrBlock(check);
    }
}

// For your own "update available" banner — local, no network, no gating:
bool canTake31 = License.IsCovered("3.1.0");
CoverageInfo cov = License.GetCoverage();   // the terms + support-until date

Que NotConfigured pase es deliberado: un producto que nunca ha configurado cobertura debe seguir funcionando, así que la exigencia es opcional por compilación y una licencia sin concesiones nunca queda bloqueada por accidente.

Activar la exigencia sin bloquear a quien ya pagó

El único momento peligroso es la primera vez que exiges en un producto que lleva años vendiendo licencias perpetuas: ninguna lleva concesión, así que un cambio ingenuo denegaría a cada cliente existente. Keyright condiciona eso a dos pasos:

# 1. How many licenses still have no coverage? This must reach zero first.
curl "$BASE/admin/products/acme-app/coverage/readiness" -H "X-Admin-Token: $TOKEN"

# 2. Give every legacy license a grant so enforcing is a no-op for them.
curl -X POST "$BASE/admin/products/acme-app/coverage/backfill" \
  -H "X-Admin-Token: $TOKEN" -H "content-type: application/json" \
  -d '{"grant":"lifetime"}'    # or a support-end date, or CSV rows

El backfill da a las licencias existentes una concesión legacy —vitalicia, una fecha de fin de soporte o filas por licencia explícitas de un CSV— de modo que la exigencia, una vez activada, no cambie nada para nadie que comprara antes de que tuvieras cobertura. Solo exiges después de que la preparación informe de cero licencias sin cobertura. Un endpoint migrate con dryRun previsualiza la ganancia antes de aplicar una concesión en todo un producto.

Aislado: la misma respuesta, sin red jamás

Una máquina que nunca puede alcanzar el servicio sigue exigiendo cobertura, porque la evaluación es local en cualquier caso. Exporta el bloque de cobertura firmado desde el panel e impórtalo en la máquina sin conexión:

License.ImportCoverageBlock(File.ReadAllText("acme.coverage.json"));
CoverageCheckResult check = await License.CheckCoverageAsync(ct: ct);  // fully offline

La importación es el único paso de cobertura que realiza un cliente aislado, y lleva su propia firma: un bloque manipulado falla la verificación y resuelve a no cubierto, no a un pase libre.

Qué conviene recordar

Mantén la validez y la cobertura separadas: una dice que la licencia es buena, la otra qué versiones puede ejecutar, y confundirlas o rompe una promesa perpetua o regala las versiones futuras. Modela la cobertura como concesiones firmadas —un rango de versión, una ventana temporal, o ambas—, juzga cada versión por su fecha de derecho y no por ningún reloj de la máquina del cliente, deja que los parches hereden la fecha de su versión base y envía el bloque firmado con el lease para que la compilación decida local y sin conexión. Antes de exigir, haz backfill de las licencias anteriores a la cobertura para que el cambio no cueste nada a tus clientes existentes. La recompensa es lo que perpetua-más-mantenimiento siempre quiso ser: una app que sigue funcionando para siempre y un flujo de actualizaciones que se detiene exactamente donde termina el derecho del cliente, decidido en el binario, sin reactivación y sin nada en la ruta crítica.

Prueba Nebula.NET

Endurece tu código .NET en minutos — empieza con la edición gratuita.