TODO: Задачи по Strapi + License Server Plugin
✅ Реализовано Content Types:
- License
- Activation
- Product
- Order / Order-Item
- Plugin-Version
- ClientCertificate
API Endpoints:
- POST /license/activate
- GET /license/validate
- POST /license/deactivate
- POST /license/heartbeat
- GET /license/status
- POST /webhooks/payment
- /products (find, findBySlug, versions)
- /orders (CRUD)
- /me/orders
- /me/licenses
- /me/downloads
- /license-server/licenses (list, get by id)
- /license-server/activations (list, get by id)
- Hydrated JSON responses для License/Activation (nested user/product/license/activations)
Services:
- crypto.js (шифрование)
- license.js (бизнес-логика)
- purchase.js (fulfillment / customer downloads / paid order delivery)
- s3.js (signed download URLs)
- validation.js
Policies:
- verify-nonce
- verify-mtls
- rate-limit (через strapi-plugin-rate-limit)
Безопасность:
- RBAC роли для buyer/support/admin сценариев (
authenticated,support,admin) - verify-mtls policy с проверкой сертификатов
- Response signing (global middleware)
- Подпись входящих запросов для
validate/heartbeat - Проверка подписи по
client_public_key - Trust levels
SIGNED/MTLS_SIGNED
Тесты:
- Unit тесты для policies
- Unit тесты для RBAC
- Unit тесты для license service
- Unit тесты для controllers
- HTTP integration tests через
supertestдля/license-server/licenses* - HTTP integration tests через
supertestдля/license-server/activations* - HTTP integration tests для admin mutation endpoints (
revoke/activate/deactivate) - Targeted plugin test suites проходят
- Targeted commerce tests проходят (
purchase,order,webhook, customer downloads) -
npm run buildпроходит - Strapi startup smoke-check проходит
- Все тесты работают с bun test (45/45)
- Targeted anti-forgery tests для
validate/heartbeatпроходят (proof-of-possession, tamper detection, no pre-verification heartbeat refresh) - Живой E2E signed/mTLS flow через
curlподтверждён (activate -> validate -> heartbeat -> deactivate)-
mTLS + signatureдаётtrust_level=MTLS_SIGNED - tampered
validatepayload отклоняется с401 INVALID_REQUEST_SIGNATURE - direct API-key fallback со valid signature даёт
trust_level=SIGNED - direct API-key fallback без подписи отклоняется (
REQUEST_SIGNATURE_REQUIRED/PAYLOAD_SIGNATURE_REQUIRED) -
heartbeatsignature failures возвращают401, а не500
-
- Полный Docker smoke с
nginx + mTLSподтверждён живым прогоном- Docker stack поднимается через
docker/pki-stack.sh up -
nginxна8443проксирует в Strapi внутри Docker-сети -
activate -> validate -> heartbeat -> deactivateпроходит через Dockernginx + mTLS + signature - запрос без client cert режется
403 - signed tampered payload режется
401 INVALID_REQUEST_SIGNATURE
- Docker stack поднимается через
- Живой customer flow проверен:
order -> payment webhook -> /me/orders -> /me/licenses -> /me/downloads -> download
База данных:
- Настройка PostgreSQL в config/database.ts (поддержка postgres/sqlite)
- PostgreSQL 17 (рекомендуемая), min 14
- Индексы для license/activation таблиц
Rate Limiting:
- strapi-plugin-rate-limit установлен
- Конфигурация в config/plugins.ts
- Per-route rules для /license/* endpoints
Redis:
- Поддержка Redis для rate-limiting (опционально)
🔲 Осталось сделать
- Дожать серверное хранение ключей до боевого порядка
- зафиксировать точный путь рядом с
docker-compose - выбрать владельца файлов и группы
- выставить права на папки, ключи и служебные файлы
- проверить, что ключи не утекают в репозиторий, лишние тома и архивы
- Выпустить боевой
intermediate CAи завести его в рабочий контур
- выпустить реальный production
intermediate CA - собрать боевую
chain - поднять
step-caс боевыми файлами - подключить
cert-signer - обновить trust в
nginx - прогнать живую проверку и негативные кейсы
- держать готовый rollback
- Закрыть offline-логику лицензии
- доделать
grace period - проверить сценарии без сети и после восстановления связи
- Добить эксплуатационные интеграции
- SendGrid
- Sentry
- Prometheus
- Инфраструктура
- Настроить PostgreSQL (config/database.ts готов)
- strapi-plugin-rate-limit настроен
- AWS S3 настроен
- Настроить Nginx с mTLS
- Создать CA сертификаты для тестирования
- Поднять Docker
nginx-edge на8443для полного локального mTLS smoke
- Безопасность
- Добавить тест CA для интеграционного тестирования
- Request signatures для
validate/heartbeat- Для activation с
client_public_keyAPI-key fallback теперь требует proof-of-possession (x-request-signature/x-payload-signature) - Подмена
license_key/device_fingerprintпосле подписи ловится проверкой подписи canonical payload -
heartbeatбольше не обновляетlast_checkinдо успешной верификации запроса
- Для activation с
- Подготовить production PKI runbook (
root CA/intermediate CA, ротация, отзыв) - Дожать серверное хранение production CA keys рядом с
docker-composeдо боевого вида- Принято решение хранить production keys на самом сервере, в локальной PKI-папке рядом с запуском Docker
- Локальное и серверное PKI-состояние разнесено по пользователям в
.docker-pki/<user>/... - Добавлен
.docker-pki/<user>/index.txtи ссылка.docker-pki/currentдля стабильного пути отdocker-compose - Добавлен
docker/pki-stack.shрядом сdocker-compose: он сам переключает активную PKI-папку и умеетup/down/restart/status - Добавлены shell-скрипты для server prep и bundle install:
prepare-production-stepca-host.sh,build-production-stepca-bundle.sh,install-production-stepca-bundle.sh - Добавлен единый server deploy script:
deploy-production-stepca-server.sh - Добавлены custody helper scripts:
verify-production-stepca-bundle.shиaudit-production-stepca-host.sh - Зафиксировать точный боевой путь на сервере и пользователя-владельца
- Выставить корректные права на папки, ключи и служебные файлы
- Проверить, что ключи не попадают в репозиторий, лишние тома и случайные архивы
- Добавлен
smallstep/step-cadev/staging path как лёгкий online CA backend -
cert-signerумеет работать поверхstep-caчерезstep ca token+step ca sign - Оформлен
step-caproduction cutover plan: custody / canary issuance / nginx trust / verification / rollback (docs-pki-runbook.md)
- Выпустить production
intermediate CAдля mTLS- Выделен отдельный
cert-signerservice под CSR signing - Добавлен
scratchDockerfile для signer service - Добавлен
step-caDocker/compose overlay для online CA backend - Strapi умеет делегировать выпуск client cert во внешний signer (
LICENSE_SIGNER_MODE=remote) - Добавлен workflow
offline root -> runtime intermediate(scripts/pki/bootstrap-intermediate-ca.sh) - Локальное PKI-состояние разнесено по пользователям в
.docker-pki/<user>/..., чтобы файлы не скапливались в одной общей куче - Добавлен
.docker-pki/<user>/index.txtи ссылка.docker-pki/currentдля стабильного локального пути - Добавлен
docker/pki-stack.shрядом сdocker-compose: он сам переключает активную PKI-папку и умеетup/down/restart/status - Docker runtime переведён на схему без root/private CA key внутри Strapi
- Оформлен production rollout/checklist:
chain/issuance/rotation/rollback(docs-pki-runbook.md) -
cert-signerвозвращает chain bundle (intermediate + root) какca_certificate -
cert-signerвозвращает actual issued cert serial, совместимый сstep-ca - Добавлен безопасный shell flow для сборки production step-ca bundle без
root keyи установки его на сервер - Добавлены rollout/rollback helper scripts:
rollout-production-intermediate.shиrollback-production-intermediate.sh - Выпустить реальный production
intermediate CAот корневого сертификата - Собрать и положить боевую
chainв серверную PKI-папку рядом сdocker-compose - Поднять
step-caс боевыми файлами и проверить выдачу черезcert-signer - Обновить доверенную цепочку в
nginxи перечитать конфигурацию - Прогнать живую проверку:
activate -> validate -> heartbeat -> deactivate - Проверить негативные кейсы: без cert / без signature / с подменённым payload
- Подготовить и проверить понятный rollback-порядок
- Выделен отдельный
- Лицензирование
- CSR processing - подпись сертификатов
- Выдача client certificates при активации
- Grace period логика (offline режим)
-
validateразличаетactive/grace_period/grace_period_expired - внутри grace лицензия остаётся валидной, но требует online
heartbeat - после истечения grace
validateтребует recovery heartbeat - первый успешный online
heartbeatвосстанавливает activation и сбрасывает offline budget - offline recovery подтверждён HTTP integration flow:
validate(expired) -> heartbeat -> validate(active)
-
- Интеграции
- AWS S3 (скачивание плагинов)
- Конфигурация в config/plugins.ts
- Provider: @strapi/provider-upload-aws-s3
- Presigned URLs для безопасной загрузки
- Service: license-server.service('s3')
- Endpoint: GET /api/license-server/products/:productId/versions/:versionId/download
- SendGrid (email уведомления)
- Sentry (мониторинг ошибок)
- Prometheus (метрики)
- Commerce / Digital Delivery
- Payment webhook fulfillment для paid order
- Human-readable license keys для VST / plugin продуктов
- Customer cabinet endpoints
/me/orders,/me/licenses,/me/downloads - Sample-pack delivery без
license_key(archive-only payload) - Frontend-friendly payload:
archive_url,archive_name,file_size_bytes,primary_download - P0: Валидировать downloadable assets до продажи / fulfillment, чтобы нельзя было продать продукт без архива / бинарника
- P0: Нормализовать модель sample-pack assets через
platform=allи fallback version lookup - P1: Зафиксировать frontend contract для
pluginvssample_packpayloads (frontend-purchase-contract.md) - P1: Добавить post-purchase delivery UX hooks (
order_reference,receipt,delivery_summary,post_purchase, CTA/email hints)-
pluginflow:license_key + downloads + primary_download -
sample_packflow:downloads only + archive_url + archive_name + file_size_bytes - Order UX fields в
/orders,/orders/:id, fulfillment:order_reference,receipt,delivery_summary,post_purchase - Frontend contract doc:
frontend-purchase-contract.md
-
- Admin UI
- Панель управления лицензиями в админке
- CRUD для License/Activation
- Revoke интерфейс
- Dashboard со статистикой
- Licenses - таблица с фильтрацией, revoke модал
- Activations - список активаций
- Products - список продуктов
- Navigation links в боковой панели
- Исправлен plugin menu path для Strapi 5 (
plugins/license-server) - Исправлена browser/Vite совместимость admin bundle через
sanitize-htmlshim - Локально устранены warnings для
License Server,Rate Limiter,useRBAC
- Документация
- Инструкция по запуску
- API документация
- Как генерировать сертификаты
- Стабилизация / Tech Debt
- Зафиксировать локальные patch'и для
strapi-plugin-rate-limitи@strapi/plugin-users-permissionsвнеnode_modules - Выбрать постоянный способ: vendor / local extension / admin alias override
-
strapi-plugin-rate-limitзавендорен вplugins/strapi-plugin-rate-limitи подключён черезconfig/plugins.ts -> resolve -
users-permissionsserver patch вынесен вsrc/extensions/users-permissions -
users-permissionsadmin patch закреплён через локальный vendor вsrc/admin/vendors/users-permissionsи alias вsrc/admin/vite.config.js
-
❓ Вопросы по Strapi (конкретные)
- Как настроить SendGrid в config/plugins.ts?
- Как отправлять email при истечении лицензии через cron?
- Как создать кастомный email template в Strapi?
- Как настроить AWS S3 для скачивания плагинов?
- Как генерировать presigned URLs для безопасной загрузки?
- Конфигурация в
config/plugins.ts:UPLOAD_PROVIDER=aws-s3AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_REGION,AWS_S3_BUCKET
- Service:
strapi.plugin('license-server').service('s3').getSignedDownloadUrl(path, expiresIn)
- Как подключить strapi-plugin-sentry?
- Как логировать ошибки валидации лицензий?
- Как добавить метрики для /license/activate endpoint?
- Как отслеживать количество активаций и revoked лицензий?
- Как добавить кастомное поле для отображения статуса лицензии?
- Как создать bulk actions для revoke/extend лицензий?
- Как добавить виджет на dashboard с статистикой?
- Как включить audit logs для отслеживания изменений лицензий?
- Какие события нужно логировать (create, update, revoke)?
- Как создать cron job для проверки истекающих лицензий?
- Как отправлять email уведомления за N дней до истечения?
Плагин: strapi-plugin-rate-limit
// config/plugins.ts
'strapi-plugin-rate-limit': {
enabled: true,
config: {
defaults: {
limit: 100,
interval: '1m',
},
redis: {
url: env('REDIS_URL'),
},
rules: [
{ path: '/api/license/activate', limit: 10, interval: '1m' },
{ path: '/api/license/heartbeat', limit: 60, interval: '1m' },
{ path: '/api/license/validate', limit: 100, interval: '1m' },
],
exclude: ['/admin/**', '/health'],
},
}Response Headers:
X-RateLimit-LimitX-RateLimit-RemainingX-RateLimit-Reset
-- License indexes
CREATE INDEX idx_license_user_id ON license (user_id);
CREATE INDEX idx_license_uid ON license (uid) UNIQUE;
CREATE INDEX idx_license_status ON license (status);
CREATE INDEX idx_license_expires_at ON license (expires_at);
-- Activation indexes
CREATE INDEX idx_activation_license_id ON activation (license_id);
CREATE INDEX idx_activation_device_fingerprint ON activation (device_fingerprint);
CREATE INDEX idx_activation_certificate_serial ON activation (certificate_serial) UNIQUE;
CREATE INDEX idx_activation_last_checkin ON activation (last_checkin);Приоритет 1: production CA custody (Vault / KMS) → убрать file-backed step-ca secrets
Приоритет 2: выпуск production intermediate CA для mTLS → rollout/runbook/checklist
Приоритет 3: SendGrid / Sentry / Prometheus
Последние обновления:
- Настроен
nginxreverse proxy для внешних/api/license/*routes с mTLS-gate дляvalidate/heartbeat - Реализованы
x-request-signatureиx-payload-signatureс проверкой поactivation.client_public_key - Для signed activations API-key path теперь требует proof-of-possession; подмена payload после подписи отклоняется
heartbeatбольше не может продлевать activation до верификации подписи/доверия- Закрыта offline/grace-period логика:
validateтеперь явно различаетactive/grace_period/grace_period_expired, а первый onlineheartbeatвосстанавливает activation после офлайна - Anti-forgery поведение подтверждено live
curl/e2e прогоном:mTLS + signatureпроходит, tampered/unsigned requests режутся - По ходу live verification исправлены два runtime бага: local signer serial response и
heartbeatsignature failure status mapping (500 -> 401) - Исправлен
global::response-sign, чтобы не ломать array-responses и Strapi admin i18n (locales.find is not a function) - Добавлены реальные HTTP integration tests для admin endpoints plugin'а License Server
- Починена hydration-логика ответов Licenses/Activations
- Починена совместимость Strapi admin с Vite browser runtime (
sanitize-htmlshim) - Локально устранены deprecated warnings в
Rate Limiterиusers-permissions - Починен RBAC bootstrap и customer JWT auth для
/api/license-server/me/*и/api/license-server/orders - Реализован reusable paid-order fulfillment и публичный
POST /api/license-server/webhooks/payment - Реализован buyer flow для
plugin:license_key + downloads - Реализован buyer flow для
sample_pack:downloads only, безlicense_key - В payload добавлены
archive_url,archive_name,file_size_bytes,primary_download - В fulfillment и order payload добавлены UX hooks:
order_reference,receipt,delivery_summary,post_purchase, CTA/email hints - Нужна отдельная постоянная стратегия для этих patch'ей, т.к. часть изменений сейчас живёт в
node_modules
Статус commerce flow
- Полный backend flow
order -> payment webhook -> fulfillment -> customer cabinet/downloadsуже собран и проверен локально - Для
pluginпокупатель получаетlicense_key+ доступ к скачиванию бинаря - Для
sample_packпокупатель получает archive-delivery payload безlicense_key - Customer API уже готов для кабинета и post-purchase UX
Сделано дополнительно
-
frontend-purchase-contract.mdфиксирует buyer/frontend contract дляpluginиsample_pack -
Commerce/backend payload теперь содержит post-purchase UX hooks для fulfillment и order history
Я добавил рабочую интеграцию с smallstep/step-ca как backend для cert-signer, не ломая текущий HTTP contract между Strapi и signer.
Что изменил
• Добавил новый backend stepca в services/cert-signer • Добавил отдельный image services/cert-signer/Dockerfile.stepca • Добавил bootstrap script: • scripts/pki/bootstrap-step-ca.sh • Добавил compose overlay: • docker/docker-compose.stepca.yml • Обновил Strapi flow, чтобы сохранялся actual issued certificate serial, а не только заранее сгенерённый placeholder • Обновил docs: • docs-pki-runbook.md • TODO.md • services/cert-signer/README.stepca.md
Важное поведение
В step-ca режиме сейчас реализован pragmatic path:
• cert-signer вызывает step ca token + step ca sign • step-ca подписывает CSR как есть • signer возвращает: • certificate • ca_certificate • fingerprint • subject_cn • serial
Это значит:
• для step-ca мы больше не зависим от того, что serial задаётся локальным signer'ом • license-server теперь хранит реальный serial выпущенного сертификата
Ограничение, о котором важно знать
С текущей реализацией step-ca не повторяет старую семантику “сервер насильно переписывает CN/SAN поверх произвольного CSR”.
То есть в step-ca mode:
• identity сертификата в основном берётся из самого CSR клиента • если вам нужен строгий CN-format вроде client::, клиентский CSR должен уже нести это значение
Это не баг текущей интеграции, а ограничение нативного CSR/token flow в step-ca.
Проверки
Я прогнал и получил успешные результаты:
• go test ./... в services/cert-signer ✅ • targeted Jest: • tests/services/crypto.test.js • tests/services/license-csr-revoke.test.js ✅ • docker compose -f docker/docker-compose.yml -f docker/docker-compose.stepca.yml config ✅ • реальный e2e smoke: • поднят step-ca • поднят cert-signer с backend stepca • отправлен реальный CSR по HTTP • ответ 200 ✅ • выдан cert + chain ✅ • пример результата: • subject_cn = client:machine-1:testhash1234 • serial = 9B6D19BD8FDBD4E89B589DDB5DDEC0A
Как запускать
-
Инициализировать dev step-ca: • bash scripts/pki/bootstrap-step-ca.sh
-
Поднять stack с overlay: • docker compose -f docker/docker-compose.yml -f docker/docker-compose.stepca.yml up
Что ещё улучшено попутно
Я специально поправил integration contract так, чтобы внешний issuer мог возвращать actual serial. Это важно не только для step-ca, но и вообще для любых CA backend'ов, которые сами выбирают serial number.
-