feat: hardware binding modes, operator management, packaged SDK
- Hardware binding: License.HwBinding (none | fixed | firstActivation).
firstActivation leaves the license unbound until the first activation,
which claims the machine; afterwards it behaves like fixed. Legacy rows
backfilled to fixed when a fingerprint was already set.
- Rebind: POST /panel/licenses/{id}/rebind moves a license to new
hardware (or releases the binding), deactivating existing activations
and re-signing a new version; 409 on revoked/expired. Operator or
company admin. Audited, emits license.rebound.
- Operator management: list, invite, promote and demote operators
(operator-only), guarding self-demotion and the last operator — until
now a second operator could only be seeded directly into the database.
- SDK packaging: CertifiEd.Client is now a proper NuGet package (id,
version, MIT license, README, XML docs, symbols) and gained the
missing public XML documentation.
This commit is contained in:
@@ -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<int>("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<T>(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.
|
||||
Reference in New Issue
Block a user