diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..cf430fc --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,70 @@ +name: CI + +on: + push: + branches: [main] + tags: ["v*"] + pull_request: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: "10.0.x" + + - name: Restore + run: dotnet restore + + # Directory.Build.props turns warnings into errors, so this also gates style. + - name: Build + run: dotnet build --configuration Release --no-restore + + - name: Pack + run: dotnet pack src/CertifiEd.Client/CertifiEd.Client.csproj --configuration Release --no-build --output artifacts + + - name: Upload package artifacts + uses: actions/upload-artifact@v3 + with: + name: nupkg + path: artifacts/* + + publish: + # Tag pushes only: v1.2.3 publishes version 1.2.3 to nuget.org. + needs: build + if: startsWith(gitea.ref, 'refs/tags/v') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: "10.0.x" + + - name: Derive version from tag + id: version + run: echo "value=${GITEA_REF#refs/tags/v}" >> "$GITHUB_OUTPUT" + env: + GITEA_REF: ${{ gitea.ref }} + + - name: Pack + run: > + dotnet pack src/CertifiEd.Client/CertifiEd.Client.csproj + --configuration Release + -p:Version=${{ steps.version.outputs.value }} + --output artifacts + + # NUGET_API_KEY must be added as a repository secret. Without it the step + # fails loudly rather than silently shipping nothing. + - name: Push to nuget.org + run: > + dotnet nuget push "artifacts/*.nupkg" + --source https://api.nuget.org/v3/index.json + --api-key "$NUGET_API_KEY" + --skip-duplicate + env: + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dcad89c --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +bin/ +obj/ +*.user +.vs/ +.idea/ +artifacts/ +*.nupkg +*.snupkg diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2075620 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,32 @@ +# Разработка и релиз + +## Сборка + +```bash +dotnet build +dotnet pack src/CertifiEd.Client/CertifiEd.Client.csproj -c Release -o artifacts +``` + +Требуется .NET 10 SDK (см. `global.json`). Предупреждения компилятора трактуются как ошибки, публичные члены обязаны иметь XML-документацию. + +## Релиз + +Версия пакета берётся из тега: + +```bash +# 1. Обновить в src/CertifiEd.Client/CertifiEd.Client.csproj +# 2. Закоммитить и поставить тег +git tag v1.0.1 +git push origin main --tags +``` + +CI (`.gitea/workflows/ci.yml`) на теге `v*` собирает пакет с версией из тега и публикует его на nuget.org. + +### Что нужно один раз настроить + +- В настройках репозитория → Actions → Secrets добавить **`NUGET_API_KEY`** — ключ nuget.org с правом публикации пакета `CertifiEd.Client`. +- Убедиться, что к репозиторию подключён Actions-раннер (Settings → Actions → Runners). Без раннера workflow не запустится. + +## Совместимость + +Публичный API следует semver: ломающие изменения — только в мажорной версии. Формат токена лицензии и протокол активации описаны в документации платформы CertifiEd. diff --git a/CertifiEd.Client.slnx b/CertifiEd.Client.slnx new file mode 100644 index 0000000..18b8efb --- /dev/null +++ b/CertifiEd.Client.slnx @@ -0,0 +1,5 @@ + + + + + diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..df177d5 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,19 @@ + + + + net10.0 + latest + enable + enable + true + + true + latest-recommended + + + + + true + + + diff --git a/global.json b/global.json new file mode 100644 index 0000000..512142d --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "10.0.100", + "rollForward": "latestFeature" + } +} diff --git a/CertifiEd.Client.csproj b/src/CertifiEd.Client/CertifiEd.Client.csproj similarity index 100% rename from CertifiEd.Client.csproj rename to src/CertifiEd.Client/CertifiEd.Client.csproj diff --git a/CertifiEdLicenseClient.cs b/src/CertifiEd.Client/CertifiEdLicenseClient.cs similarity index 100% rename from CertifiEdLicenseClient.cs rename to src/CertifiEd.Client/CertifiEdLicenseClient.cs diff --git a/HwFingerprint.cs b/src/CertifiEd.Client/HwFingerprint.cs similarity index 100% rename from HwFingerprint.cs rename to src/CertifiEd.Client/HwFingerprint.cs diff --git a/src/CertifiEd.Client/LICENSE b/src/CertifiEd.Client/LICENSE new file mode 100644 index 0000000..21ac3b5 --- /dev/null +++ b/src/CertifiEd.Client/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/src/CertifiEd.Client/README.md b/src/CertifiEd.Client/README.md new file mode 100644 index 0000000..e37e0dc --- /dev/null +++ b/src/CertifiEd.Client/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.