RSRemnawave ShopЛицензии

Лицензирование панели

Панель проверяет ключ у нас и работает на одном домене. Ниже — что вызывать, что приходит в ответ и как вести себя, когда лицензионный сервер недоступен.

С чего начать

  1. Получите ключ: оформите заявку в кабинете.
  2. Впишите ключ и домен в переменные панели.
  3. При первом запуске панель вызывает /api/v1/activate и занимает домен.
  4. Дальше раз в сутки — /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, поэтому отдельной настройки не требуется.

Документация по лицензиям — Remnawave Shop