Клиентский SDK (CertifiEd.Client)
Лёгкий .NET-SDK, который встраивается в ваше приложение и берёт на себя весь клиентский протокол: загрузку файла лицензии, оффлайн-проверку подписи Ed25519, активацию, фоновые heartbeat'ы с кэшированием offline-маркера и деактивацию при завершении.
Тарҷумаи тоҷикии ҳуҷҷатҳо дар ҳоли омодашавӣ аст. Матни зерин ба забони русӣ оварда шудааст.
SDK зависит только от формата токена и NSec.Cryptography — он не тянет за собой ни Domain, ни Infrastructure сервера.
Установка
SDK поставляется NuGet-пакетом `CertifiEd.Client` 1.0.0 (лицензия MIT, автор Ofarandagon). Пакет несёт XML-документацию для IntelliSense и symbols-пакет .snupkg; единственная зависимость — NSec.Cryptography.
dotnet add package CertifiEd.ClientProjectReference
Вариант для моно-репозитория или сборки из исходников:
<ItemGroup> <ProjectReference Include="path/to/src/CertifiEd.Client/CertifiEd.Client.csproj" /></ItemGroup>Локальный NuGet-фид
Вариант для поставки SDK клиентам отдельно от кода сервера:
# build the packagedotnet pack src/CertifiEd.Client -c Release -o ./nupkg
# register the local source and installdotnet nuget add source ./nupkg --name certified-localdotnet add package CertifiEd.Client --source certified-localПубличный API
| Член | Назначение |
|---|---|
new CertifiEdLicenseClient(CertifiEdClientOptions options, HttpClient? http = null) | Конструктор. Если http не передан, SDK создаёт свой HttpClient с BaseAddress = ServerUrl и владеет им |
Task<bool> InitializeAsync(ct) | Грузит .ced, оффлайн-проверяет подпись по PublicKey, активируется на сервере (best-effort) и запускает heartbeat-петлю. false — файла нет / подпись невалидна / токен истёк |
CertifiEdLicenseStatus Status { get; } | Invalid / Active / GracePeriod / Expired / Revoked — выводится из exp токена и маркера |
bool HasFeature(string name) | Есть ли name в массиве features подписанного cfg |
T? GetConfig<T>(string path) where T : struct | Значение по dotted-path ("limits.maxSeats") из cfg; null, если пути нет |
ValueTask DisposeAsync() | Best-effort deactivate и остановка таймера heartbeat |
CertifiEdClientOptions
| Свойство | Обязательное | Назначение |
|---|---|---|
ServerUrl | да | Базовый URL сервера, например https://api.certified.example |
LicenseFilePath | да | Путь к локальному .ced — конверт или «голый» токен |
PublicKey | да | «Сырые» 32 байта Ed25519 (Convert.FromHexString(hex)) |
HwFingerprint | нет | Переопределяет фингерпринт; по умолчанию HwFingerprint.Get() |
Полный пример интеграции
using CertifiEd.Client;
// The public key ships with your product (from the .ced file or GET .../public-key).// The API returns it as hex — unpack it into raw bytes on the client.byte[] publicKey = Convert.FromHexString( "7bac1e3ce8578ad859bfe001a4f4d72702d00e3d1eebdd735ebdb7145d74f35b");
var options = new CertifiEdClientOptions{ ServerUrl = "https://api.certified.example", LicenseFilePath = "/etc/myapp/license.ced", PublicKey = publicKey, // HwFingerprint = "..." // optional; defaults to HwFingerprint.Get()};
await using var license = new CertifiEdLicenseClient(options);
if (!await license.InitializeAsync()){ Console.Error.WriteLine("License missing, corrupted or expired."); return 1;}
// Gate on statusswitch (license.Status){ case CertifiEdLicenseStatus.Active: break; // all good
case CertifiEdLicenseStatus.GracePeriod: Console.WriteLine("License is in the grace period — restore connectivity."); break; // keep running, but warn
case CertifiEdLicenseStatus.Expired: case CertifiEdLicenseStatus.Revoked: case CertifiEdLicenseStatus.Invalid: Console.Error.WriteLine($"Access denied: {license.Status}."); return 1; // block}
// Gate on features and limitsif (license.HasFeature("export")) EnableExport();
int maxSeats = license.GetConfig<int>("limits.maxSeats") ?? 1; // fallback defaultEnforceSeatLimit(maxSeats);
RunApplication();return 0;// await using calls DisposeAsync -> deactivate + stop the heartbeat timer.GetConfig<T> работает с where T : struct — int, long, bool, double, DateTimeOffset и т. п. Для строк и массивов читайте cfg напрямую либо используйте HasFeature.
Что делает InitializeAsync
- Читает файл (
.ced-конверт или «голый» токен) и парсит три сегмента. - Оффлайн проверяет подпись Ed25519 по
PublicKey. Провал → токен сбрасывается, метод возвращаетfalse. - Проверяет
exp: истёкший токен →false. - Загружает закэшированный маркер
<license>.ced.hb, если он есть. - Best-effort активация
POST /api/v1/client/activate: при успехе сохраняетactivationId, применяет свежий маркер и запускает таймер heartbeat.
Оффлайн-поведение
- Пока приложение периодически берёт свежий маркер — статус
Active. - Пропала сеть: маркер держит
Activeдо своегоexp(= момент выдачи +maxOfflineDays), затем 24 часаGracePeriod, затемExpired. - Рестарт без сети: маркер читается с диска, лицензия остаётся валидной до
expмаркера. - Жёсткий
expтокена перекрывает всё: после него статус всегдаExpired, даже онлайн.
Подробности вычисления статуса — в разделе Протокол лицензий.
Обработка Expired и Revoked
- Expired — срок вышел (
token.expпрошёл) либо маркер и grace истекли без связи. Обычно требует перевыпуска лицензии оператором. - Revoked — лицензия отозвана на сервере. SDK узнаёт об этом косвенно: сервер отклоняет активацию и heartbeat (
license.not_active), свежие маркеры перестают приходить, и по истечении окна оффлайна статус деградирует доExpired.
Практика: гейтите функциональность по Status == Active || Status == GracePeriod, в GracePeriod показывайте предупреждение, на Expired / Revoked / Invalid — блокируйте защищённые возможности.
Привязка к оборудованию
SDK всегда отправляет hwFingerprint в POST /api/v1/client/activate. По умолчанию это HwFingerprint.Get() — стабильный SHA-256 по имени машины, описанию ОС, первому стабильному MAC-адресу и platform machine id, вычисляется один раз за процесс. Своё значение (например, стабильный per-tenant идентификатор) задаётся через CertifiEdClientOptions.HwFingerprint.
| Режим лицензии | Что видит интегратор |
|---|---|
none | Отпечаток пишется в активацию, но ничего не ограничивает |
fixed | Активация проходит только на машине с заданным отпечатком |
firstActivation | Первый запуск закрепляет машину; дальше — как fixed |
Полное описание режимов и перепривязки — в разделе Привязка к железу.
Что видит клиент при mismatch
Активация возвращает 400:
{ "code": "activation.fingerprint_mismatch", "message": "License is bound to different hardware.", "status": 400}SDK не роняет InitializeAsync из-за этого: активация — best-effort, а решение о доступе принимается по Status, который выводится из локального токена и маркера. Практический эффект такой:
- Новые offline-маркеры перестают приходить — heartbeat невозможен без
activationId. - Пока жив последний маркер, статус остаётся
Active, затемGracePeriod, затемExpired. - На «чистой» машине, где маркера не было, отсчёт идёт от последней локальной валидации и лицензия деградирует через тот же
maxOfflineDays.
Замена машины
Отпечаток на стороне клиента не «чинится» — перепривязка делается на платформе:
- Оператор или Admin компании вызывает
POST /api/v1/panel/licenses/{id}/rebindс новымhwFingerprint(илиnull, чтобы лицензия закрепилась на следующей машине сама). - Платформа деактивирует старые активации и переподписывает лицензию новой версией.
- Заказчик заново скачивает `.ced` (
GET /api/v1/panel/licenses/{id}/downloadили портал) и кладёт файл по путиLicenseFilePath. - Старый кэш маркера
<license>.ced.hbможно удалить — новый придёт с первой успешной активацией.
Публичный ключ при этом не меняется (если не было ротации ключа шаблона), так что зашитый в приложение PublicKey остаётся валидным.
Жизненный цикл и Dispose
- Вызывайте
DisposeAsyncровно один раз — черезawait usingили явно. SDK владеетHttpClient, который создал сам, и закрывает его; повторный вызов обратится к уже закрытому клиенту. - Если вы передали свой
HttpClientв конструктор — SDK его не закрывает, управляйте им сами. DisposeAsyncшлёт best-effortdeactivate; недоступность сервера не мешает корректному завершению.