Лицензирование панели
Панель проверяет ключ у нас и работает на одном домене. Ниже — что вызывать, что приходит в ответ и как вести себя, когда лицензионный сервер недоступен.
С чего начать
- Получите ключ: оформите заявку в кабинете.
- Впишите ключ и домен в переменные панели.
- При первом запуске панель вызывает /api/v1/activate и занимает домен.
- Дальше раз в сутки — /api/v1/validate.
Активация
Вызывается один раз. Если у ключа ещё нет домена, он закрепляется за тем, что вы передали. Повторный вызов с тем же доменом безопасен.
Запрос
POST https://license.angelic.foo/api/v1/activate
content-type: application/json
{
"key": "RWP-XXXX-XXXX-XXXX-XXXX",
"domain": "shop.example.com",
"version": "1.4.2"
}Ответ
200 OK
{
"valid": true,
"status": "ACTIVE",
"plan": "Pro",
"domain": "shop.example.com",
"issuedAt": "2026-09-11T10:00:00.000Z",
"expiresAt": "2027-09-11T10:00:00.000Z",
"checkedAt": "2026-09-11T10:00:03.000Z",
"graceDays": 7,
"data": "eyJ2YWxpZCI6dHJ1ZSwi…",
"signature": "b3kQ0m…"
}Ежедневная проверка
/api/v1/validate принимает то же тело и отвечает так же, но домен не занимает: несовпадение сразу считается ошибкой. Поле versionнеобязательное, оно показывается в админке и помогает понять, какая сборка у клиента.
Вызов из панели
// раз в сутки, плюс один раз при старте приложения
const answer = await fetch('https://license.angelic.foo/api/v1/validate', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
key: process.env.LICENSE_KEY,
domain: process.env.WEB_DOMAIN,
version: process.env.APP_VERSION,
}),
}).then((response) => response.json());
if (answer.valid) {
await saveLicenseCache(answer); // слепок с подписью
} else {
const cached = await readLicenseCache(); // работаем по кэшу не дольше graceDays
if (!cached || olderThan(cached, 7)) enterReadOnlyMode(answer.reason);
}Отказы
У неуспешного ответа valid: false, а в status — причина. Подписи в таком ответе нет.
403 DOMAIN_MISMATCH ключ привязан к другому домену 403 SUSPENDED лицензия приостановлена 403 REVOKED лицензия отозвана 403 EXPIRED срок лицензии истёк 404 NOT_FOUND такого ключа нет 409 NOT_ACTIVATED ключ ещё не активирован на домене 409 DOMAIN_TAKEN домен занят другой лицензией 429 RATE_LIMITED слишком частые обращения
Подпись ответа
Успешный ответ содержит слепок data и подпись signature (Ed25519). Публичный ключ отдаётся по адресу https://license.angelic.foo/api/v1/public-key и не меняется.
Проверка на стороне панели
import { createPublicKey, verify } from 'node:crypto';
const PUBLIC_KEY = createPublicKey(process.env.LICENSE_PUBLIC_KEY);
/**
* Проверка подписанного ответа лицензионного сервера. Подпись Ed25519 стоит
* над base64url-слепком ответа: перед доверием кэшу сверяем её и домен.
*/
export function readLicense(answer, expectedDomain) {
const ok = verify(null, Buffer.from(answer.data), PUBLIC_KEY, Buffer.from(answer.signature, 'base64url'));
if (!ok) throw new Error('Подпись лицензии не сходится');
const payload = JSON.parse(Buffer.from(answer.data, 'base64url').toString());
if (payload.domain !== expectedDomain) throw new Error('Лицензия выдана другому домену');
if (payload.expiresAt && new Date(payload.expiresAt) < new Date()) throw new Error('Лицензия истекла');
return payload;
}Работа без связи
Сеть падает, и панель не должна закрываться из-за этого. Храните последний подписанный ответ и считайте лицензию действующей ещё graceDays (сейчас семь дней) после последней успешной проверки. Отказ с явной причиной — отозвана, истекла, чужой домен — кэшем не перекрывается.
Переменные панели
# .env панели LICENSE_KEY=RWP-XXXX-XXXX-XXXX-XXXX LICENSE_API=https://license.angelic.foo LICENSE_PUBLIC_KEY="$(curl -s https://license.angelic.foo/api/v1/public-key)"
Домен берётся из той же переменной, что уже используется для Caddy, поэтому отдельной настройки не требуется.