# Спецификация лицензирования TTRPG Player (этап D1)

Документ фиксирует модель **D1**: онлайн-активация, срок, число устройств, отзыв, и как это согласуется с клиентом и сервером лицензий.

Исходный код сервера вынесен в отдельный репозиторий: [DndGamePlayerLicenseServer](https://git.mailib.ru/ifontosh/DndGamePlayerLicenseServer.git).

## Модель

1. **Продуктовый ключ** — секрет покупателя, известен только ему и серверу. Обменивается на **лицензионный токен** через `POST /v1/activate` (онлайн-активация). В поле «Указать ключ» приложение принимает **продуктовый ключ** `TTRPG-…` или устаревший `DND-…` (клиент сам вызывает `POST /v1/activate` с `deviceId`) или уже готовый **токен** (две части base64url через одну точку). Новые ключи на сервере — только `TTRPG-…` (см. репозиторий лицензий).
2. **Лицензионный токен** — публичная полезная нагрузка (`sub`, `pid`, `iat`, `exp`, `did`) + подпись **Ed25519**. Клиент хранит только токен и **публичный** ключ (вшит в приложение); подделать валидный токен без приватного ключа сервера невозможно.
3. **Срок** — поле `exp` (unix секунды) в выданном токене. Клиент отклоняет истёкший токен без сети. На сервере два формата продуктового ключа:
   - **fixed** — в записи ключа задан `expiresAtSec`; при активации `exp` копируется из него (старый формат).
   - **period** — задан `validDays`; при **первой** активации лицензии (`sub`) сервер фиксирует `activatedAtSec` и выставляет `exp = activatedAtSec + validDays×86400` для всех устройств. Клиенту тип ключа неизвестен — только итоговый `exp` в токене.
4. **Устройства** — поле `did` в токене: при активации сервер привязывает токен к `deviceId` клиента и ведёт учёт списка устройств на `sub` в `data.json` (`maxDevices`). `deviceId` — **отпечаток физической машины** (не пользователь ОС): Windows `MachineGuid`, macOS `IOPlatformUUID`, Linux `/etc/machine-id`; клиент хеширует значение (SHA-256) и шлёт opaque-строку. На одном ПК разные учётки Windows/macOS/Linux получают один и тот же `deviceId`. Опционально `retireDeviceId` в `POST /v1/activate` снимает старый слот (миграция с per-user UUID).
5. **Отзыв** — сервер помечает `sub` в `revokedSubs`. Клиент при наличии `DND_LICENSE_STATUS_URL` запрашивает `GET /v1/status?sub=…`; при `revoked: true` лицензия считается недействительной **без обновления** приложения. Офлайн до истечения `exp` отозванный токен формально криптографически валиден — это осознанный компромисс; при необходимости сокращайте срок жизни токена или добавляйте принудительную онлайн-проверку перед критичными действиями.

## Продакшен-сборка

Скрипт `npm run build` / `node scripts/build.mjs --production` подставляет в main-бандл базовый URL **`https://license.ttrpgplayer.ru/`** как `process.env.DND_LICENSE_STATUS_URL` (если при сборке переменная не задана). Другой хост: `DND_LICENSE_STATUS_URL=https://example.com npm run build`. В dev по-прежнему можно выставить переменную окружения при запуске Electron без пересборки.

## Хранение на клиенте

Токен не хранится открытым текстом в JSON userData: используется **Electron `safeStorage`** (на macOS — связка с Keychain, на Windows — DPAPI). Идентификатор устройства вычисляется из ID машины ОС (см. выше); файл `device.id` в userData — устаревший per-user UUID, читается только для совместимости до повторной активации. Принятие EULA — `preferences.json` (версия текста).

Для тестов/отладки можно задать `DND_LICENSE_DEVICE_ID` (готовая строка deviceId, без fingerprint).

### Linux / WSL без keyring

На Linux `safeStorage` обычно требует **Secret Service** (например `gnome-keyring` + D-Bus). В **WSL** без keyring `safeStorage.isEncryptionAvailable()` часто **false**, и сохранить токен нельзя.

Явный обход (только если осознанно нужен запуск без OS-хранилища): переменная окружения **`DND_LICENSE_INSECURE_FILE_STORAGE=1`**. Тогда токен пишется в файл **`license.sealed.fallback`** (AES-256-GCM, ключ от `deviceId` + константа приложения). Это **слабее**, чем связка с ОС: при копировании `userData` + знании формата теоретически проще атаковать офлайн. Для обычного десктопа Linux с рабочим сеансом переменную не задавайте.

Пример запуска AppImage:

```bash
DND_LICENSE_INSECURE_FILE_STORAGE=1 ./TTRPGPlayer-1.0.12-x64.AppImage --no-sandbox --appimage-extract-and-run
```

## Юридическое (D9)

Текст EULA в приложении (`app/renderer/legal/eulaRu.ts`) и формулировки про активацию/отзыв/устройства. Перед первым вводом ключа пользователь принимает EULA (версия `EULA_CURRENT_VERSION` в `app/shared/license/eulaVersion.ts`).
