Skip to content

Премиум-аддоны и лицензирование

Эта страница показывает, как продавать ваше дополнение в официальном маркетплейсе Pano и заставить Pano убедиться, что оно работает только на серверах, которые действительно за него заплатили.

Продажа работает через точно тот же поток релиза, что и бесплатное дополнение, плюс две дополнительные части: встроенный во время сборки ключ лицензии в вашем jar (единственный скомпилированный файл, в котором отгружается ваше дополнение) и проверка лицензии во время выполнения в вашем коде. Иными словами: когда вы компилируете дополнение, вы включаете внутрь него небольшой файл ключа проверки (это часть времени сборки), а когда дополнение запускается, оно использует этот ключ, чтобы проверить, что сервер за него заплатил (это часть времени выполнения). Это полное прохождение, на которое указывает Сборка и публикация.

Если вы ещё не выпустили бесплатное дополнение, сначала прочтите Сборку и публикацию — всё там (релизная сборка, версионирование, каналы релизов, .releaserc.json, создание ресурса и API-токена) всё ещё применимо. Эта страница лишь добавляет премиум-слой поверх.

Платные ресурсы пока недоступны сторонним авторам

Премиум сейчас доступен только аккаунтам с доступом к панели на panomc.com — как и freemium, и для дополнений, и для тем. Продажа в маркетплейсе сторонним авторам пока не открыта, поэтому на шаге выбора цены оба варианта отключены, а API отклоняет их для всех остальных. Эта страница описывает, как работает премиум, чтобы вы могли планировать; пока программа не открыта, публикуйте дополнение бесплатным.

TL;DR — переход на премиум — это три небольших изменения

Большая часть этой страницы — фон. Реальная работа крошечная:

  1. Добавьте один флаг сборки (или одну переменную окружения CI), чтобы ваш jar собирался с ключом лицензии — Шаг 1.
  2. Сохраните существующую проверку лицензии boilerplate — два коротких метода, которые уже есть у свежего pano-boilerplate-pluginШаг 2.
  3. Опубликуйте как платный ресурс — выберите ценообразование paid и задайте useGitHubLink: falseШаг 3.

Затем протестируйте сами. Всё под Как это работает под капотом — необязательное чтение.

Никакая защита не абсолютна

Будьте честны с собой о том, что эта система может и не может делать: ни одна система лицензирования не может защитить код на 100%. Цель здесь — сделать несанкционированное использование как можно более трудным для подавляющего большинства пользователей, а не сделать его невозможным. Как и любое ПО, любой код, попадающий в руки конечного пользователя, по своей сути открыт: достаточно решительный и умелый человек всегда сможет разобрать его. Это верно для любого DRM, когда-либо созданного, не только для Pano. Оценивайте и поддерживайте ваше дополнение с учётом этой реальности.

Шаг 1 — Встройте ключ лицензии во время сборки

Премиум-сборка принимает несколько дополнительных входов, которые помещают публичный ключ проверки внутрь вашего jar. Это опции, которые вы передаёте при сборке — в командной строке или как переменные окружения в вашей автоматической сборке — а не то, что вы пишете в какой-либо файл в вашем репозитории (смотрите Конфигурация манифеста → Свойства премиум-сборки).

Если вы не передаёте ни одного из них, ваше дополнение собирается как обычный бесплатный jar, и проверка во время выполнения не делает ничего вообще («no-op») — так что тот же исходник может отгружаться бесплатным или премиум без изменений кода.

Вы никогда не помечаете дополнение «премиум» в файле конфигурации

Дополнение является премиум исключительно потому, что вы собрали его с ключом лицензии — ничего больше. Собрали с ключом → премиум; собрали без → бесплатное, и между ними нет изменений кода. Вы не редактируете никакой файл конфигурации, и нет поля premium нигде (ни в gradle.properties, ни в манифесте jar). (Темы работают иначе — они переключают "premium": true в manifest.json — но к дополнениям это не относится.)

Свойство Gradle — опция, которую вы добавляете к команде сборки, например ./gradlew build -PlicenseServer=prod. Переменная окружения — именованное значение, которое ваш CI (ваша автоматическая сборка, например на GitHub Actions) устанавливает перед запуском сборки. Вот все входы, которые вы можете использовать:

ВходВидЧто делает
-PlicenseServer=dev|prod|<url>Свойство GradleПолучает публичный ключ с сервера лицензий. Передайте одно из dev, prod или полный кастомный URL — devhttps://api-dev.panomc.com, prodhttps://api.panomc.com. Рекомендуется — нечего секретного хранить.
PANO_LICENSE_SERVERПеременная окруженияТо же, что -PlicenseServer, для CI (установите на шаге сборки Gradle). Используется, когда свойство не задано.
-PpanoLicensePublicKey=<base64>Свойство GradleПоставляет ключ напрямую (Base64 или PEM — два распространённых текстовых кодирования ключа; вы получили бы значение с panomc.com, и большинству эта опция никогда не нужна), пропуская любой сетевой вызов.
PANO_LICENSE_PUBLIC_KEYПеременная окруженияТо же, что -PpanoLicensePublicKey, для CI. Используется, когда свойство не задано.

Какой вход побеждает? Сборка использует первый из установленных:

  1. -PpanoLicensePublicKey — ключ, который вы поставили напрямую в командной строке
  2. PANO_LICENSE_PUBLIC_KEY — то же, из переменной окружения
  3. -PlicenseServer — получить ключ с сервера лицензий
  4. PANO_LICENSE_SERVER — то же, из переменной окружения

Если ни один из них не установлен, встроенный ключ пуст, и jar бесплатный.

Простейшая премиум-сборка:

bash
./gradlew build -PlicenseServer=prod

Теперь вы должны увидеть

jar в build/libs/, собранный с впечённым публичным ключом panomc.com. Нет отдельного флага «премиум» для установки или файла конфигурации, чтобы вглядываться — входы сборки, которые вы передали (Шаг 1), — это то, что решает, был ли встроен ключ проверки, а с ним и проверка лицензии. Надёжный способ подтвердить, что премиум-поведение реально работает, — прогнать его от начала до конца в Протестируйте сами.

Для CI (вашей автоматической сборки на GitHub Actions) установите PANO_LICENSE_SERVER на шаге сборки Gradle вашего workflow — например dev на ветке dev и prod на main — чтобы пуш производил корректно ключеванный jar. Стандартный workflow релиза pano-boilerplate-plugin не устанавливает его: его шаг сборки — обычный ./gradlew build, так что из коробки пуш производит бесплатный jar. Добавление этой переменной окружения на шаг сборки — единственное изменение, которое делает премиум-форк (gradle.properties из boilerplate явно об этом упоминает).

Конкретно это означает добавление блока env: на шаг сборки. Сниппет ниже иллюстративен — сопоставьте его с шагом сборки вашего собственного workflow:

yaml
      # your existing release workflow, build step
      - name: Build
        env:
          PANO_LICENSE_SERVER: ${{ github.ref_name == 'main' && 'prod' || 'dev' }}
        run: ./gradlew build

Теперь вы должны увидеть

Пуш в вашу ветку релиза теперь производит премиум jar вместо бесплатного (prod на main, dev на вашей dev-ветке).

Ключ публичный — выбор флага о удобстве, а не о секретности

Встроенное значение — это публичный ключ проверки panomc.com, так что скрывать нечего (это в отличие от вашего API-токена маркетплейса, который должен оставаться секретом). Предпочитайте -PlicenseServer / PANO_LICENSE_SERVER, чтобы сборка всегда получала текущий ключ; используйте -PpanoLicensePublicKey / PANO_LICENSE_PUBLIC_KEY только когда ваш CI не может достучаться до сервера лицензий во время сборки.

Всегда делайте чистую полную сборку для премиум-релиза

Как и для любого релизного jar, никогда не собирайте премиум-релиз с -Pnoui — этот флаг пропускает пересборку веб-UI дополнения, так что старая копия UI может оказаться внутри jar (смотрите выноску в Сборке и публикации). Флаг лицензии — отдельная забота: добавьте его к обычной, чистой ./gradlew build, а не к dev-сборке с -Pnoui.

Шаг 2 — Добавьте проверку лицензии во время выполнения

Проверка живёт в вашем классе плагина и использует PluginLicenseClient из boilerplate.

Если вы начали с boilerplate, этот код уже существует — ваша единственная задача подтвердить, что он всё ещё там. Свежий pano-boilerplate-plugin уже готов к лицензии и просто ведёт себя как бесплатный, пока вы не соберёте его с ключом (Шаг 1). Проверьте, что onStart() всё ещё начинается с licenseClient.requireValidLicense(). Если вы его удалили или добавляете премиум к существующему дополнению, добавьте следующее:

kotlin
class ShoutboxPlugin : PanoPlugin() {
    private val licenseClient by lazy { PluginLicenseClient(this) }

    override suspend fun onStart() {
        licenseClient.requireValidLicense()  // no-op for free builds; throws if premium & invalid
        // ... the rest of your startup
    }

    override suspend fun verifyLicense() {
        licenseClient.requireValidLicense()  // backs the panel's "Refresh license" button
    }
}

Два кусочка синтаксиса Kotlin в этом сниппете, на случай, если они для вас новы: by lazy { ... } просто означает «создать этот объект в первый раз, когда он используется, не раньше», а suspend помечает функцию, которую Pano выполняет как корутину (свой способ обработки конкурентности). Оба несущие — копируйте сигнатуры точно.

  • requireValidLicense() — это ворота. На премиум-jar она получает, проверяет и перекрёстно проверяет лицензию; на бесплатном jar (без встроенного ключа) она немедленно возвращается. Вызывайте её в начале onStart(), чтобы, если лицензия недействительна, брошенная ею ошибка немедленно останавливала ваше дополнение, до того как выполнится любой ваш код запуска.
  • verifyLicense() — переопределяемый хук PanoPlugin (метод, который Pano предоставляет вам для заполнения), который кнопка Обновить лицензию панели вызывает после того, как хост получил свежий JWT — так что панель отражает реальный, текущий результат, а не устаревший. Вам не нужна здесь никакая кастомная логика — просто вызовите тот же requireValidLicense() внутри неё, как показано.

Когда requireValidLicense() терпит неудачу, она бросает LicenseRequiredException. PF4J (движок плагинов внутри Pano — ничего, с чем вы взаимодействуете напрямую) затем помечает только ваше дополнение как упавшее, а хост (платформа Pano, в которую установлено ваше дополнение) записывает причину; ядро Pano и все другие дополнения продолжают работать. Сохранение неломаемости платформы одним неправильно настроенным премиум-дополнением намеренно — оператор всё ещё может достучаться до своей панели, чтобы это решить.

Дополнительные проверки, усложняющие взлом: LicenseGuard (необязательно)

Глубокая защита просто означает не полагаться на одну-единственную проверку. Взломщик (тот, кто пытается пиратить ваше дополнение) может попытаться удалить единственный вызов requireValidLicense() в onStart. Чтобы усложнить это, boilerplate также поставляет LicenseGuard, который позволяет добавить однострочные перепроверки в код, который выполняется чаще всего — ваши API-эндпоинты (обработчики маршрутов), запланированные задачи, обработчики WebSocket. Эти перепроверки почти бесплатны, потому что переиспользуют уже полученную лицензию вместо обращения к panomc.com каждый раз:

kotlin
override suspend fun handle(context: RoutingContext): Result {
    LicenseGuard.assert(plugin)
    // ... business logic
}

Здесь plugin — ваш экземпляр PanoPlugin — передавайте ту переменную, которая его держит (например, эндпоинт, которому передали плагин при его создании — «внедрение через конструктор» — это просто то, что фреймворк передаёт плагин в конструктор эндпоинта за вас). Если лицензия недействительна, assert останавливает вызов так же, как основная проверка (бросая), вместо того чтобы пропустить запрос. LicenseGuard.assert(plugin) переиспользует кэшированную лицензию и перезапрашивает только если она истекла, так что стоимость ничтожна — но чем в большем числе мест она появляется, тем больше правок нужно сделать взломщику.

Этот шаг необязателен и может подождать, пока ваше дополнение реально не начнёт продаваться.

Шаг 3 — Опубликуйте как платный ресурс

Публикация премиум-дополнения использует те же три шага, что и бесплатного — создать ресурс, создать API-токен, позволить автоматизации загрузить версии — из Публикации в официальном маркетплейсе Pano. Две вещи отличаются:

1. Оцените как платное. На форме Create resource на panomc.com выберите опцию ценообразования paid вместо free. Как всегда для дополнений, ваш resourceId в маркетплейсе — точно ваш pluginId — смотрите подсказку про ID ресурса. (Темы получают случайно сгенерированный ID под названием UUID, но дополнения просто переиспользуют свой pluginId.)

2. Загрузите jar напрямую — не используйте useGitHubLink. Бесплатные дополнения задают useGitHubLink: true, чтобы избежать дублирующей загрузки. Премиум-дополнения должны позволить маркетплейсу держать официальную эталонную копию jar и записать её SHA-256 (её отпечаток — смотрите Как это работает под капотом), потому что этот записанный хеш — то, к чему привязывается лицензия каждого покупателя. В конфигурации плагина Pano в вашем .releaserc.json уберите useGitHubLink (или задайте его в false).

Блок ниже показан как diff: строка, начинающаяся с -, — та, что вы удаляете, строка, начинающаяся с +, — та, что вы добавляете, а всё остальное — включая плейсхолдер ..., который заменяет вашу существующую конфигурацию, оставленную без изменений — остаётся ровно как у вас уже есть:

diff
 ["@PanoMC/semantic-release-pano", {
   "file": "build/libs/pano-plugin-shoutbox-${version}.jar",
   "panoVersion": "1.0.0",
-  "useGitHubLink": true,
+  "useGitHubLink": false,
   "repositoryUrl": "https://github.com/YourName/pano-plugin-shoutbox.git",
   "configs": [ ... ]
 }]

Всё остальное в этой конфигурации — две записи каналов, tokenVar, branches — не изменяется относительно разбора .releaserc.json.

Что происходит, когда покупатель запускает его

Когда Pano покупателя запускает ваше премиум-дополнение, panomc.com выдаёт токен лицензии только если этот подключённый аккаунт купил ваш ресурс и SHA-256 работающего jar совпадает с хешем, записанным для этой версии. (Вы, как автор ресурса, всегда проходите эту проверку — что и позволяет вам тестировать своё собственное платное дополнение в Протестируйте сами.) Вот почему напрямую загруженный, записанный по хешу jar важен: это отпечаток, на котором держится вся проверка.

Что покупатели видят в своей панели

Если премиум-дополнение не может себя лицензировать, оператор не остаётся гадать — панель отражает это в трёх местах: значок статуса на каждое дополнение на Панель → Дополнения, карточка лицензии на странице деталей дополнения и баннер на дашборде. Частые статусы и их исправления:

СтатусЗначениеИсправление
LICENSEDДействительная лицензия, дополнение работает.Ничего делать не нужно.
NO_PURCHASEАккаунт подключён, но не купил это дополнение.Купите его на panomc.com (карточка даёт ссылку).
NOT_CONNECTEDНет аккаунта panomc.com, подключённого к этому Pano.Подключите аккаунт в настройках панели.
NETWORK_ERRORpanomc.com был недоступен во время проверки.Восстановите соединение, затем нажмите Обновить.
JAR_TAMPEREDХеш работающего jar не совпадает с лицензированным.Перескачайте дополнение с panomc.com.

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

Протестируйте сами

Прежде чем с кого-то брать деньги, прогоните весь цикл против вашего собственного аккаунта. Вы всегда проходите проверку покупки для вашего собственного ресурса, так что это предполагаемый способ проверить премиум-дополнение от начала до конца:

  1. Опубликуйте версию через ваш обычный поток релиза в предрелизный канал (например, ваш dev/alpha-канал). Публикация — это то, что записывает SHA-256-хеш jar в маркетплейсе — без записанного хеша лицензии не к чему привязываться.
  2. Получите ровно этот jar на локальный Pano. Либо соберите ту же версию с соответствующим сервером лицензий — ./gradlew build -PlicenseServer=dev для канала dev — либо скачайте опубликованный jar. Он должен быть побайтово той версией, чей хеш был записан на шаге 1.
  3. Подключите ваш авторский аккаунт panomc.com к вашему локальному Pano в настройках панели (то же действие «подключить аккаунт», которое использует реальный покупатель).
  4. Запустите дополнение.

Теперь вы должны увидеть

Значок LICENSED на Панель → Дополнения и нормально работающее дополнение. Если вы вместо этого видите NOT_CONNECTED, завершите шаг 3; NO_PURCHASE на вашем собственном ресурсе обычно означает, что подключённый аккаунт — не ваш авторский аккаунт.

Опционально докажите, что проверка подделки работает: измените один байт в jar и запустите его снова — хеш больше не совпадает, и статус переключается на JAR_TAMPERED.

Как это работает под капотом

Всё ниже — фон. Вам это не нужно, чтобы отгрузить премиум-дополнение, но это объясняет, почему шагов выше достаточно.

Крипто-ликбез за 30 секунд

Pano использует асимметричную подпись. panomc.com держит секретный приватный ключ, который может создавать подписанные токены; все остальные получают соответствующий публичный ключ, который может только проверять эти подписи, но никогда их создавать. Вот почему безопасно впекать публичный ключ в ваш jar — он может проверять, но не может подделывать. RS256 — просто название этого алгоритма подписи.

Несколько терминов, которые использует эта страница:

  • Публичный ключ / приватный ключ — пара выше. Публичный ключ может проверять; приватный ключ (хранимый panomc.com) может подписывать. Вот почему «публичный» ключ безопасно раздавать.
  • JWT — небольшой подписанный текстовый токен, несущий claims (факты) вроде «этот сервер купил это дополнение». Любой с публичным ключом может проверить его, но никто без приватного ключа не может подделать. Подделать токен означает создать фальшивый, который всё ещё проходит проверку подписи — асимметричная подпись делает это практически невозможным.
  • Хеш SHA-256 — отпечаток, вычисленный из точных байтов файла. Измените хоть один байт, и отпечаток меняется полностью.
  • Хост — платформа Pano, в которую установлено ваше дополнение (самостоятельно размещённый сервер покупателя).

Как работает система лицензирования (простыми словами)

Премиум-дополнение несёт копию публичного ключа panomc.com, впечённую во время сборки. Оттуда:

  1. Когда Pano запускает ваше дополнение, дополнение просит хост получить недолговечный (1 час) подписанный токен лицензии — JWT — с panomc.com.
  2. Дополнение затем перепроверяет этот токен само, используя собственный встроенный публичный ключ — оно не доверяет хосту сделать это.
  3. Токен привязан к четырём вещам: этой конкретной установке Pano (идентифицированной через её подключённый аккаунт panomc.com), вашему ресурсу, версии дополнения и хешу SHA-256 работающего jar. Токен, выданный для одного сервера, версии или jar, бесполезен где-либо ещё.
  4. Если проверка проходит, дополнение стартует нормально. Если она не проходит, дополнение отказывается стартовать — но сам Pano продолжает работать, и сбой отражается в панели, чтобы оператор мог его исправить.

Важный момент дизайна, простыми словами: проверка, которая имеет значение, происходит внутри вашего дополнения, а не внутри Pano. Ядро Pano — открытый исходный код, его можно форкнуть или пропатчить, так что дополнение никогда не доверяет хосту проверку лицензии — оно перепроверяет подпись JWT само, своим собственным встроенным публичным ключом. Вот что означает «код вашего плагина — граница безопасности». Даже подделанный хост не может подделать токен, потому что у него нет приватного ключа panomc.com.

Что на самом деле проверяет requireValidLicense()

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

  1. Немедленно возвращается, если дополнение не премиум (нет встроенного ключа).
  2. Просит хост (getLicenseManager()) получить лицензию для resourceId = pluginId и встроенной версии.
  3. Перепроверяет подпись JWT встроенным публичным ключом, против ожидаемого издателя — идентичности, от которой токен претендует происходить, то есть panomc.com (getLicenseJwtIssuer()). Это реальная граница безопасности.
  4. Подтверждает, что ресурс токена совпадает с вашим pluginId, версия совпадает, а хеш jar SHA-256 совпадает с работающим jar (getOwnJarSha256()).
  5. Подтверждает, что токен не истёк, затем кэширует его.

Отпечаток и привязка к версии

Для дополнений отпечаток лицензии — SHA-256 jar. Токен несёт этот хеш, а во время выполнения ваше дополнение сравнивает его с хешем jar, который оно реально запускает (getOwnJarSha256()). Если кто-то переупакует или пропатчит jar, хеш больше не совпадает с лицензированным, и проверка падает со статусом подделки.

Держите версии в движении через релизы

Лицензия выдаётся на версию + jar. Если вы патчите или редактируете вручную jar, который уже был выпущен, его хеш больше не совпадает с записанным для этой версии, так что он больше не будет лицензирован. Всегда отгружайте изменения как новый релиз через обычный поток; это единственный правильный путь. Свежий хеш на каждом релизе — также фича: он инвалидирует любую работу по взлому, проделанную против предыдущей сборки.

Заметки об усилении защиты

Система уже наслаивает несколько защит — привязка на установку/версию/jar, перепроверка подписи со стороны плагина, короткие 1-часовые токены и разбросанные вызовы LicenseGuard. Вы можете поднять планку ещё выше:

  • Рассыпьте LicenseGuard.assert(plugin) широко, чтобы ни одна удалённая проверка не отключала ворота.
  • Отгружайте каждое исправление как новый релиз, поскольку каждый новый хеш jar заставляет заново тратить усилия на взлом.
  • Обфусцируйте ваш jar. Обфускация переименовывает ваши скомпилированные классы и методы в бессмысленные символы, так что взломщик не может легко найти код лицензии, чтобы удалить его. ProGuard — стандартный бесплатный инструмент. Это необязательно и может подождать, пока ваше дополнение реально не начнёт продаваться.

Но держите в голове предупреждение о честности в начале этой страницы: эти меры повышают стоимость взлома для подавляющего большинства пользователей; они не делают его невозможным.

Куда дальше

  • Сборка и публикация — релизная сборка, версионирование и полный поток публикации в маркетплейс, на котором строится эта страница.
  • Конфигурация манифеста — свойства премиум-сборки в контексте остальной части gradle.properties.
  • Разработка бэкенда — где живут ваш класс PanoPlugin, эндпоинты и жизненный цикл onStart.
  • Темы используют аналогичную схему отпечатка всего zip — смотрите премиум-темы.