diff --git a/CertifiEd.Client.csproj b/CertifiEd.Client.csproj index 4369227..cda9793 100644 --- a/CertifiEd.Client.csproj +++ b/CertifiEd.Client.csproj @@ -1,9 +1,37 @@ - + CertifiEd client SDK: license activation, heartbeats and offline Ed25519 token validation. + + true + + + CertifiEd.Client + 1.0.0 + Ofarandagon + Ofarandagon + CertifiEd + Copyright © Ofarandagon 2026 + licensing;license;ed25519;offline;activation;self-hosted + https://git.ofarandagon.tj/Ofarandagon/CertifiEd + https://git.ofarandagon.tj/Ofarandagon/CertifiEd + git + MIT + README.md + false + + true + snupkg + true + true + + + + + + diff --git a/CertifiEdLicenseClient.cs b/CertifiEdLicenseClient.cs index 4b6950c..7218029 100644 --- a/CertifiEdLicenseClient.cs +++ b/CertifiEdLicenseClient.cs @@ -35,6 +35,9 @@ public sealed class CertifiEdLicenseClient : IAsyncDisposable private DateTimeOffset _lastValidatedAt; private bool _disposed; + /// Creates a client for a single local license file. + /// Server URL, license path and the Ed25519 public key. + /// Optional shared ; when omitted the client owns and disposes its own. public CertifiEdLicenseClient(CertifiEdClientOptions options, HttpClient? http = null) { ArgumentNullException.ThrowIfNull(options); @@ -189,6 +192,10 @@ public sealed class CertifiEdLicenseClient : IAsyncDisposable return true; } + /// + /// Stops the heartbeat loop and best-effort deactivates this machine, freeing its seat. + /// Safe to call more than once. + /// public async ValueTask DisposeAsync() { if (_disposed) @@ -431,6 +438,7 @@ public sealed class CertifiEdLicenseClient : IAsyncDisposable // ── Options ─────────────────────────────────────────────────────────────────── +/// Configuration for . public sealed class CertifiEdClientOptions { /// Base URL of the CertifiEd server, e.g. "https://licensing.example.com". @@ -448,12 +456,22 @@ public sealed class CertifiEdClientOptions // ── Status ──────────────────────────────────────────────────────────────────── +/// Local license state, derived from the token and the cached heartbeat marker. public enum CertifiEdLicenseStatus { + /// No license loaded, or its signature did not verify. Invalid, + + /// Licensed — the token is valid and within its offline window. Active, + + /// Offline window elapsed; still usable for a short grace period. GracePeriod, + + /// The license term or the grace period has run out. Expired, + + /// The license was revoked by the issuer. Revoked, } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..21ac3b5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Ofarandagon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e37e0dc --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +# CertifiEd.Client + +Клиентский SDK для [CertifiEd](https://git.ofarandagon.tj/Ofarandagon/CertifiEd) — self-hosted платформы лицензирования on-prem ПО от [Ofarandagon](https://ofarandagon.tj). + +SDK проверяет лицензию **локально** по публичному ключу Ed25519: приложение работает без связи с сервером до 30 дней, а конфигурация (feature-флаги и лимиты) читается из самого подписанного токена — подделать нельзя. + +## Установка + +```bash +dotnet add package CertifiEd.Client +``` + +## Быстрый старт + +```csharp +using CertifiEd.Client; + +await using var license = new CertifiEdLicenseClient(new CertifiEdClientOptions +{ + ServerUrl = "https://api.licensing.example.com", + LicenseFilePath = "license.ced", // выданный клиенту файл + PublicKey = Convert.FromHexString(publicKeyHex), // 32 байта Ed25519 +}); + +if (!await license.InitializeAsync()) +{ + Console.Error.WriteLine("Лицензия отсутствует, повреждена, подделана или истекла."); + return 1; +} + +if (license.Status is not (CertifiEdLicenseStatus.Active or CertifiEdLicenseStatus.GracePeriod)) + return 1; + +if (license.HasFeature("reports.export")) + EnableReportExport(); + +var seats = license.GetConfig("limits.maxSeats") ?? 1; +``` + +`InitializeAsync` загружает токен, **офлайн** проверяет подпись Ed25519 и срок, затем (best-effort) активируется на сервере и запускает фоновый heartbeat. Отсутствие сети не роняет приложение. + +Публичный ключ выдаёт панель или API: `GET /api/v1/client/public-key/{licenseKey}` (hex → `Convert.FromHexString`). Ключ безопасно зашивать в приложение. + +## API + +| Член | Назначение | +|---|---| +| `InitializeAsync(ct)` | Загрузка + офлайн-проверка подписи, активация, старт heartbeat. `false` — лицензия невалидна | +| `Status` | `Active` · `GracePeriod` · `Expired` · `Invalid` · `Revoked` | +| `HasFeature(name)` | Флаг из массива `features` подписанного конфига | +| `GetConfig(path)` | Значение по dotted-path, напр. `"limits.maxSeats"` | +| `DisposeAsync()` | Best-effort деактивация (освобождает место) и остановка таймера. Идемпотентен | + +### Опции + +| Опция | Описание | +|---|---| +| `ServerUrl` | Базовый URL сервера CertifiEd | +| `LicenseFilePath` | Путь к `.ced` (голый токен или скачанный JSON-конверт) | +| `PublicKey` | Сырые 32 байта публичного ключа Ed25519 | +| `HwFingerprint` | Переопределить отпечаток железа (по умолчанию вычисляется автоматически) | + +## Офлайн-режим + +После каждого успешного heartbeat сервер возвращает подписанный маркер; SDK кэширует его рядом с файлом лицензии (`license.ced.hb`). Перезапуски без сети остаются лицензированными, пока маркер жив (до `maxOfflineDays`, максимум 30). Далее — `GracePeriod`, затем `Expired`. + +Рекомендация: не блокируйте работу при первом же сбое сети — ориентируйтесь на `Status`, а не на факт ответа сервера. + +## Привязка к железу + +Лицензия может быть привязана к машине по аппаратному отпечатку. Отпечаток вычисляется автоматически (`HwFingerprint.Get()`), при необходимости переопределяется через `CertifiEdClientOptions.HwFingerprint` — например, если у вас своя стабильная идентификация узла. + +Если лицензия привязана к другой машине, активация вернёт ошибку несовпадения отпечатка. Перепривязка (замена оборудования) выполняется на стороне платформы оператором или администратором тенанта. + +## Обработка ошибок + +- `InitializeAsync` возвращает `false` — файла нет, токен повреждён/подделан или срок истёк. Не бросает при недоступном сервере. +- Сетевые сбои и таймауты внутри активации/heartbeat проглатываются осознанно — работает офлайн-окно. +- Проверяйте `Status` перед выдачей платной функциональности, а не только результат `InitializeAsync`. + +## Документация + +Полная документация — в [`docs/`](https://git.ofarandagon.tj/Ofarandagon/CertifiEd) репозитория: протокол лицензирования, справочник API, webhooks и руководство по SDK. Рабочий пример — `examples/CertifiEd.QuickStart`. + +--- + +© Ofarandagon, 2026. SDK распространяется по лицензии MIT.