Meridian Office — лицензирование, аккаунты и биллинг
Раскрытие раздела 9 библии (
docs/00-foundation.md) до уровня реализации. Решения библии (редакции, формат ключаMRDN-…, лицензионный файл.mlicна CBOR с подписью Ed25519, способы активации, состав сервисов) не меняются; всё, что требует решения владельца, вынесено в раздел 16, предложения отступить от библии — в раздел «Вопросы к библии» в самом конце. Версия 0.1 · 2026-10-03 · Машиночитаемые спецификации:spec/license/license-key-format.md,spec/license/license-file.schema.json,spec/license/api-license-service.md,spec/license/db-schema.sql.
1. Цели и принципы
| # | Принцип | Что это значит в реализации |
|---|---|---|
| P1 | Честным удобно, пиратам неудобно | Активация — одно поле ввода или один вход в аккаунт; переустановка ОС не «съедает» место; перенос устройства — самообслуживание. Взлом не предотвращается полностью, но требует усилий больше, чем стоит лицензия: подпись Ed25519, проверки в нескольких местах ядра, серверная валидация подписок, отзыв партий ключей. |
| P2 | Документы никогда не блокируются | Любой файл открывается, читается, печатается и экспортируется в PDF в любой редакции и в любом состоянии лицензии (истекла, отозвана, нет сети). Истечение переводит приложение в режим «только чтение и печать», а не в «окно оплаты». |
| P3 | Офлайн-первый | Лицензия — локальный подписанный файл, проверяемый без сети при каждом запуске. Сеть нужна только для получения лицензии и периодической проверки подписки (раз в 7 дней, льготный период 30 дней). Enterprise работает в полностью изолированной сети через on-prem сервер или офлайн-файлы. |
| P4 | Аккаунт не обязателен для Free и Home | Free — вообще без ключа и аккаунта. Home — ключ без аккаунта (онлайн-обмен ключа на лицензию) или через аккаунт по желанию. Аккаунт обязателен там, где без него нельзя технически: подписки, облако, совместная работа, Assist, организации. |
| P5 | Один механизм гейтинга | Все проверки редакции — только
Features::has(Feature::X); UI показывает «замок» и
предложение апгрейда. Ни одной «разбросанной» проверки по
приложениям. |
| P6 | Никаких персональных данных в лицензии и отпечатке | Отпечаток устройства — солёные SHA-256 от идентификаторов железа; имя устройства — только по согласию, редактируемое. |
| P7 | Серверы заменяемы | Клиент говорит с любым сервером, реализующим
api-license-service.md: облачный издателя, on-prem
Enterprise, тестовый. Адрес сервера — поле issuer в
лицензии. |
| P8 | Коммерция с фазы 0 | Схема редакций, ключей, лицензий и каркас сервисов — часть фазы 0; продажи Home/Pro — фаза 1 (см. раздел 15). |
Терминология (используется далее везде):
| Термин | Определение |
|---|---|
| Ключ (key) | строка MRDN-XXXXX-… на 125 бит; ссылка на запись
сервера; сам ничего не разблокирует |
| Право (entitlement) | серверная запись «кто имеет право на какую редакцию, на сколько мест, до какого срока»; создаётся покупкой, ключом, грантом, пробным периодом |
| Активация (activation) | связывание одного устройства с правом; расходует одно место (seat) |
Лицензия (license, .mlic) |
подписанный Ed25519 файл для конкретного устройства, результат активации; проверяется офлайн |
| Место (seat) | единица права: устройство (Home/Pro), пользователь (Business/Education), активация (объёмный ключ), одновременный сеанс (плавающая) |
| Аренда (lease) | краткосрочная лицензия от плавающего сервера (60 мин, продление в фоне) |
| CRL | подписанный список отзыва лицензий, ключей и партий |
2. Редакции, матрица функций, пробный период
2.1 Редакции (из библии, без изменений)
| Код | Редакция | Мест по умолчанию | Аккаунт | Модели ключа (kind) |
|---|---|---|---|---|
free |
Free | — | нет | ключа нет |
home |
Home | 3 устройства | нет (по желанию) | perpetual, subscription |
pro |
Pro | 5 устройств | обязателен для подписки/облака; perpetual без облака —
без аккаунта |
subscription, perpetual |
business |
Business | N пользователей | обязателен (организация) | subscription |
enterprise |
Enterprise | N пользователей / N активаций / пул | организация; офлайн-режим допускает отсутствие аккаунтов | subscription, volume,
float_server, perpetual (договор) |
education |
Education | класс (30) / кампус (N) | организация | subscription |
trial |
Trial | 1 устройство | нет | trial (локальный или серверный) |
2.2 Флаги функций
Feature::*
Единственный источник истины о том, что входит в редакцию. Флаги — и
в клиенте (enum), и в лицензии (features[] — строковые
имена в snake_case), и на сервере (таблица
edition_features). Лицензия всегда содержит
явный список; клиент не вычисляет набор по имени
редакции (кроме Free и локального Trial, у которых наборы зашиты).
// kernel/crates/meridian-license/src/feature.rs
/// Флаг функции / Feature flag. Имена в лицензии — snake_case от варианта.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, strum::EnumString, strum::Display, strum::EnumIter)]
#[strum(serialize_all = "snake_case")]
#[repr(u16)]
pub enum Feature {
// --- Ядро: всегда включено, в лицензии не передаётся / Core: always on ---
CoreHub, CoreWrite, CoreSheets, CoreSlides, CoreMath, PdfView, FormatsAll, Print, ExportPdf,
TrackChanges, Comments, Templates, SpellCheck, Hyphenation,
// --- Приложения / Applications ---
AppPaint, AppDraw, AppNotes, AppMail, AppCalendar, AppBase, AppForms, AppPublisher, AppPlan,
// --- PDF ---
PdfEdit, PdfForms, PdfSign, PdfConvert, PdfOcr,
// --- Write ---
WriteCompare, WriteMailMerge, WriteMasterDoc, WriteBibliography,
// --- Sheets ---
SheetsExternalData, SheetsSolver, SheetsLargeGrid, SheetsDbConnectors,
// --- Slides / Draw ---
SlidesVideoExport, SlidesPresenterRemote, DrawVsdxExport, DrawStencilsPro,
// --- Скрипты / Scripts ---
ScriptsRun, ScriptsEdit, ScriptsStore, ScriptsPython, ScriptsVbaTranslate, ScriptsOrgSigned,
// --- Облако и совместная работа / Cloud & collaboration ---
CloudStorage, CloudCollab, CloudShare, CloudVersionHistory, CloudOnPrem,
// --- Assist ---
AssistLocal, AssistCloud,
// --- Forms ---
FormsPublish, FormsOrgResponses,
// --- Безопасность документов / Document security ---
DocSign, DocSignGost, DocEncrypt,
// --- Организация / Organization ---
OrgConsole, OrgSso, OrgPolicies, OrgKeyDistribution, OrgAudit, OrgScim, OrgCloudQuota,
// --- Enterprise ---
OfflineActivation, FloatingLicense, VolumeActivation, OnPremServer, CustomBuild,
// --- Поддержка и обновления / Support & updates ---
SupportPriority, SupportSla, UpdateChannelBeta, UpdateChannelLts,
// --- Шаблоны и магазин / Templates & store ---
TemplatesPremium, TemplateStore,
}
impl Feature {
/// Ядро не гейтится никогда / Core features are never gated.
pub const fn is_core(self) -> bool {
matches!(self, Self::CoreHub | Self::CoreWrite | Self::CoreSheets | Self::CoreSlides | Self::CoreMath
| Self::PdfView | Self::FormatsAll | Self::Print | Self::ExportPdf | Self::TrackChanges
| Self::Comments | Self::Templates | Self::SpellCheck | Self::Hyphenation)
}
}
/// Набор флагов — битовая карта u128 / Feature set as a u128 bitmap.
#[derive(Clone, Copy, Default, PartialEq, Eq)]
pub struct Features(u128);
impl Features {
pub fn has(&self, f: Feature) -> bool { self.0 & (1u128 << (f as u16)) != 0 || f.is_core() }
pub fn with(mut self, f: Feature) -> Self { self.0 |= 1u128 << (f as u16); self }
pub fn from_names<'a>(names: impl IntoIterator<Item = &'a str>) -> Self {
names.into_iter().filter_map(|n| n.parse::<Feature>().ok()).fold(Self::default(), Self::with)
}
}Группы по редакциям (сервер хранит то же в
edition_features; это стартовая конфигурация, изменяемая
без выпуска клиента, потому что лицензия несёт явный список):
| Группа | Флаги | Free | Home | Pro | Business | Enterprise | Education |
|---|---|---|---|---|---|---|---|
| Ядро | core_*, pdf_view,
formats_all, print, export_pdf,
track_changes, comments,
templates, spell_check,
hyphenation |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Домашние приложения | app_paint, app_draw,
app_notes, app_mail,
app_calendar, pdf_edit,
pdf_forms, app_forms,
scripts_run, scripts_edit,
scripts_store |
— | ✓ | ✓ | ✓ | ✓ | ✓ |
| Профессиональные | app_base, assist_local,
assist_cloud, cloud_storage,
cloud_collab, cloud_share,
cloud_version_history, support_priority,
app_publisher, app_plan,
pdf_sign, pdf_convert, pdf_ocr,
write_*, sheets_*, slides_*,
draw_*, scripts_python,
scripts_vba_translate, doc_sign,
doc_encrypt, templates_premium,
template_store, forms_publish,
update_channel_beta |
— | — | ✓ | ✓ | ✓ | ✓ (без assist_cloud по умолчанию) |
| Организация | org_console, org_sso,
org_policies, org_key_distribution,
org_audit, org_scim,
org_cloud_quota, scripts_org_signed,
forms_org_responses, update_channel_lts |
— | — | — | ✓ | ✓ | ✓ |
| Enterprise | offline_activation, floating_license,
volume_activation, on_prem_server,
cloud_on_prem, custom_build,
doc_sign_gost, support_sla |
— | — | — | — | ✓ | по договору |
2.3 Матрица функций (функция → редакция)
Обозначения: ✓ — доступно; — недоступно (замок + предложение апгрейда); R — только чтение и печать (файл открывается, печатается, экспортируется в PDF, редактирование и сохранение недоступны); число — лимит. Строки, помеченные (предл.), — предложение там, где библия молчит.
| # | Приложение | Функция | Флаг | Free | Home | Pro | Business | Enterprise | Education |
|---|---|---|---|---|---|---|---|---|---|
| 1 | Hub | Стартовое окно, недавние, шаблоны, поиск | core_hub |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| 2 | Hub | Облачные файлы в Hub | cloud_storage |
— | — | ✓ | ✓ | ✓ | ✓ |
| 3 | Hub | Магазин шаблонов (премиум) (предл.) | template_store |
— | — | ✓ | ✓ | ✓ | ✓ |
| 4 | Hub | Каналы обновлений beta/nightly | update_channel_beta |
— | — | ✓ | ✓ | ✓ | ✓ |
| 5 | Hub | Канал LTS (отложенные обновления) (предл.) | update_channel_lts |
— | — | — | ✓ | ✓ | ✓ |
| 6 | Write | Документы, форматирование, стили, все форматы R/W | core_write |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| 7 | Write | Рецензирование: исправления, комментарии | track_changes |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| 8 | Write | Сравнение и объединение документов (предл.) | write_compare |
— | — | ✓ | ✓ | ✓ | ✓ |
| 9 | Write | Слияние (рассылка писем) (предл.) | write_mail_merge |
— | — | ✓ | ✓ | ✓ | ✓ |
| 10 | Write | Составные документы (книги из глав) (предл.) | write_master_doc |
— | — | ✓ | ✓ | ✓ | ✓ |
| 11 | Write | Библиография и цитирование (BibTeX/CSL) (предл.) | write_bibliography |
— | — | ✓ | ✓ | ✓ | ✓ |
| 12 | Write | Экспорт PDF/A, PDF/UA, EPUB, HTML, Markdown | export_pdf |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| 13 | Sheets | Формулы (500+), форматирование, диаграммы, сводные таблицы | core_sheets |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| 14 | Sheets | Внешние данные (web, JSON, parquet, обновляемые запросы) (предл.) | sheets_external_data |
— | — | ✓ | ✓ | ✓ | ✓ |
| 15 | Sheets | Подключения к СУБД (PostgreSQL, MySQL, MS SQL, ODBC) (предл.) | sheets_db_connectors |
— | — | ✓ | ✓ | ✓ | ✓ |
| 16 | Sheets | Поиск решения (solver), сценарии (предл.) | sheets_solver |
— | — | ✓ | ✓ | ✓ | ✓ |
| 17 | Sheets | Большие сетки (> 1 млн строк, out-of-core) (предл.) | sheets_large_grid |
1 млн | 1 млн | ✓ | ✓ | ✓ | ✓ |
| 18 | Slides | Презентации, анимации, режим докладчика | core_slides |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| 19 | Slides | Экспорт в видео (mp4/webm) (предл.) | slides_video_export |
— | — | ✓ | ✓ | ✓ | ✓ |
| 20 | Slides | Управление показом с телефона (локальная сеть) (предл.) | slides_presenter_remote |
— | — | ✓ | ✓ | ✓ | ✓ |
| 21 | Draw | Векторные схемы, коннекторы, svg/odg/vsdx чтение | app_draw |
— | ✓ | ✓ | ✓ | ✓ | ✓ |
| 22 | Draw | Экспорт в .vsdx (предл.) | draw_vsdx_export |
— | — | ✓ | ✓ | ✓ | ✓ |
| 23 | Draw | Профессиональные наборы трафаретов (сети, UML, BPMN, планы) (предл.) | draw_stencils_pro |
— | — | ✓ | ✓ | ✓ | ✓ |
| 24 | Paint | Растровый редактор, слои, фильтры, скриншоты | app_paint |
— | ✓ | ✓ | ✓ | ✓ | ✓ |
| 25 | Просмотр, поиск, печать, заполнение форм без сохранения | pdf_view |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
| 26 | Аннотации, редактирование текста/страниц, объединение | pdf_edit |
— | ✓ | ✓ | ✓ | ✓ | ✓ | |
| 27 | Создание и сохранение форм | pdf_forms |
— | ✓ | ✓ | ✓ | ✓ | ✓ | |
| 28 | Электронная подпись PDF (PAdES) (предл.) | pdf_sign |
— | — | ✓ | ✓ | ✓ | ✓ | |
| 29 | Конвертация PDF → .docx/.mwx (через Write) (предл.) | pdf_convert |
— | — | ✓ | ✓ | ✓ | ✓ | |
| 30 | OCR сканов (локальный движок) (предл.) | pdf_ocr |
— | — | ✓ | ✓ | ✓ | ✓ | |
| 31 | Math | Редактор формул во всех приложениях | core_math |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| 32 | Notes | Записные книжки, рукописный ввод, вырезки | app_notes |
— | ✓ | ✓ | ✓ | ✓ | ✓ |
| 33 | Почта, контакты, задачи | app_mail |
— | ✓ | ✓ | ✓ | ✓ | ✓ | |
| 34 | Календарь (CalDAV, Exchange/EWS) | app_calendar |
— | ✓ | ✓ | ✓ | ✓ | ✓ | |
| 35 | Base | Настольные БД, формы, отчёты | app_base |
— | — | ✓ | ✓ | ✓ | ✓ |
| 36 | Base | Коннекторы к серверным СУБД | sheets_db_connectors |
— | — | ✓ | ✓ | ✓ | ✓ |
| 37 | Forms | Личные формы, сбор ответов в локальный .msx | app_forms |
— | ✓ | ✓ | ✓ | ✓ | ✓ |
| 38 | Forms | Публикация форм в облаке (ссылка, QR) (предл.) | forms_publish |
— | — | ✓ | ✓ | ✓ | ✓ |
| 39 | Forms | Ответы в организацию, SSO-респонденты (предл.) | forms_org_responses |
— | — | — | ✓ | ✓ | ✓ |
| 40 | Publisher | Настольная вёрстка (фаза 7) | app_publisher |
— | — | ✓ | ✓ | ✓ | ✓ |
| 41 | Plan | Планирование проектов (фаза 7) | app_plan |
— | — | ✓ | ✓ | ✓ | ✓ |
| 42 | Все | Запуск скриптов из документа (с разрешения) | scripts_run |
— | ✓ | ✓ | ✓ | ✓ | ✓ |
| 43 | Все | Редактор скриптов, отладчик | scripts_edit |
— | ✓ | ✓ | ✓ | ✓ | ✓ |
| 44 | Все | Магазин скриптов и плагинов | scripts_store |
— | ✓ | ✓ | ✓ | ✓ | ✓ |
| 45 | Все | Python-скрипты (плагин CPython) (предл.) | scripts_python |
— | — | ✓ | ✓ | ✓ | ✓ |
| 46 | Все | Транслятор VBA → MeridianScript (предл.) | scripts_vba_translate |
— | — | ✓ | ✓ | ✓ | ✓ |
| 47 | Все | Запуск только скриптов, подписанных организацией | scripts_org_signed |
— | — | — | ✓ | ✓ | ✓ |
| 48 | Все | Meridian Cloud: хранилище | cloud_storage |
— | — | 100 ГБ | 1 ТБ/орг | по договору | 100 ГБ/польз. |
| 49 | Все | Совместное редактирование (CRDT) | cloud_collab |
— | — | ✓ | ✓ | ✓ | ✓ |
| 50 | Все | Общий доступ по ссылке, права | cloud_share |
— | — | ✓ | ✓ | ✓ | ✓ |
| 51 | Все | История версий в облаке (90 дней / 1 год) (предл.) | cloud_version_history |
— | — | 90 д | 1 год | ∞ | 1 год |
| 52 | Все | Своё облако (on-prem Meridian Cloud) | cloud_on_prem |
— | — | — | — | ✓ | по договору |
| 53 | Все | Assist: локальные модели (переписать, подытожить) | assist_local |
— | — | ✓ | ✓ | ✓ | ✓ |
| 54 | Все | Assist: облачные модели (квота запросов/мес) | assist_cloud |
— | — | 2000 | 2000/польз. | договор | выкл. по умолч. |
| 55 | Все | Подпись документов (X.509, PAdES/XAdES) (предл.) | doc_sign |
— | — | ✓ | ✓ | ✓ | ✓ |
| 56 | Все | Подпись по ГОСТ Р 34.10-2012 (КриптоПро/VipNet CSP) | doc_sign_gost |
— | — | — | — | ✓ | по договору |
| 57 | Все | Шифрование документов паролем (AES-256) (предл.) | doc_encrypt |
— | — | ✓ | ✓ | ✓ | ✓ |
| 58 | Все | Премиум-шаблоны (предл.) | templates_premium |
— | — | ✓ | ✓ | ✓ | ✓ |
| 59 | Все | Приоритетная поддержка (ответ ≤ 1 раб. дня) | support_priority |
— | — | ✓ | ✓ | ✓ | ✓ |
| 60 | Все | SLA-поддержка (24/7, выделенный инженер) | support_sla |
— | — | — | — | ✓ | — |
| 61 | Орг. | Консоль администратора (веб) | org_console |
— | — | — | ✓ | ✓ | ✓ |
| 62 | Орг. | SSO (OIDC/SAML 2.0) | org_sso |
— | — | — | ✓ | ✓ | ✓ |
| 63 | Орг. | Политики (облако, скрипты, обновления, Assist) | org_policies |
— | — | — | ✓ | ✓ | ✓ |
| 64 | Орг. | Централизованная раздача мест/ключей | org_key_distribution |
— | — | — | ✓ | ✓ | ✓ |
| 65 | Орг. | Журнал аудита (1 год / 3 года) | org_audit |
— | — | — | 1 год | 3 года | 1 год |
| 66 | Орг. | SCIM 2.0 провиженинг | org_scim |
— | — | — | ✓ | ✓ | ✓ |
| 67 | Орг. | Квота облака организации | org_cloud_quota |
— | — | — | 1 ТБ | договор | 1 ТБ |
| 68 | Ent. | Офлайн-активация (.mreq/.mlic) | offline_activation |
— | — | — | — | ✓ | по договору |
| 69 | Ent. | Плавающие лицензии (on-prem сервер) | floating_license |
— | — | — | — | ✓ | по договору |
| 70 | Ent. | Объёмная активация (один ключ на N мест, MDM/GPO) | volume_activation |
— | — | — | — | ✓ | ✓ (кампус) |
| 71 | Ent. | On-prem комплект сервисов (id/license/cloud/update) | on_prem_server |
— | — | — | — | ✓ | по договору |
| 72 | Ent. | Собственные сборки (брендинг, набор приложений, политики в сборке) | custom_build |
— | — | — | — | ✓ | — |
| 73 | Все | Печать, предпросмотр, PDF/A | print |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| 74 | Все | Проверка орфографии/грамматики, переносы | spell_check |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| 75 | Все | Устройств на пользователя | limits.devices |
∞ | 3 | 5 | 5 (предл.) | политика | 3 (предл.) |
| 76 | Все | Коммерческое использование (предл.) | — (EULA) | нет | нет | да | да | да | нет (учебное) |
Правило для R (только чтение и печать): если флаг
приложения (app_*) отсутствует, а файл этого приложения
открыт (например, .mpi в Free или .mbx после
истечения Pro) — приложение запускается в режиме ReadOnly:
открыть, листать, искать, копировать, печатать, экспортировать в
PDF/PNG; команды правки — с замком; «Сохранить» недоступно, «Сохранить
как» → только экспорт. Для Write/Sheets/Slides/PDF-просмотра/Math режим
R не наступает никогда (ядро Free).
2.4 Пробный период (Trial)
- Что: редакция
proна 30 дней, без карты, без аккаунта. Предлагается в Hub при первом запуске («Попробовать Pro 30 дней» / «Продолжить в Free»); можно включить позже из Параметров. - Как выдаётся: серверный trial —
POST /v1/trial/startс отпечатком устройства →.mlicсkind=trial,expires_at = now + 30 д. Без сети — локальный trial: клиент создаёт самоподписанную (ключом устройства) записьtrial.localв хранилище ОС + файл-метку; при первом же выходе в сеть она обменивается на серверную с сохранением исходной даты начала (сервер принимаетstarted_atне раньше, чем за 30 дней). - Повторный trial на том же отпечатке: сервер отказывает
(
TRIAL_ALREADY_USED) в течение 12 месяцев и до следующей мажорной версии; локальный trial защищён только меткой в хранилище ОС (переустановка ОС его сбрасывает — принимаем, P1 важнее). - Партнёрские trial-ключи (
edition=trial,kind=trial, 60–90 дней) — для вузов, обзоров, акций; активируются как обычный ключ.
Состояния пробного периода:
| Состояние | Условие | Поведение UI | Переходы |
|---|---|---|---|
TrialOffered |
первый запуск, нет лицензии, trial не использован | карточка в Hub с двумя кнопками | → TrialActive (согласие) / → NotActivated
(Free) |
TrialActive(n) |
лицензия kind=trial, n дней до
expires_at, n > 7 |
бейдж «Pro · пробный период, N дн.» в заголовке Hub | → TrialExpiring при n ≤ 7 |
TrialExpiring(n) |
n ≤ 7 | жёлтый баннер в Hub и backstage: «Пробный период заканчивается через N дн.» + «Купить Pro» + «Ввести ключ» | → TrialExpired / → Active при покупке |
TrialExpired |
now ≥ expires_at |
один раз — диалог с предложением; затем состояние = Free, Pro-приложения в R | → Active при покупке/ключе |
TrialUnavailable |
сервер ответил TRIAL_ALREADY_USED |
карточки trial нет; в Параметрах — «Пробный период уже использован на этом устройстве» | → Active |
Что происходит после истечения (таблица):
| Было в Trial (Pro) | После истечения |
|---|---|
| Write, Sheets, Slides, PDF-просмотр, Math | работают полностью (Free) |
| Paint, Draw, Notes, Mail, Forms, PDF-редактор, Base, Publisher, Plan | R — только чтение и печать; созданные файлы открываются; экспорт в PDF/PNG/CSV доступен |
| Облако (100 ГБ) | только чтение и скачивание 90 дней, затем удаление по уведомлению (3 письма) |
| Совместная работа | сессии закрываются; локальная копия сохраняется с историей |
| Assist | замок |
| Скрипты | не запускаются (кроме подписанных издателем системных) |
| Документы с Pro-функциями (например, библиография) | открываются и печатаются в Write; Pro-поля не редактируются, но сохраняются при пересохранении (round-trip) |
3. Ценообразование (ориентир для решения владельца)
Все цифры — ориентир для обсуждения, не решение. Валюты: ₽ для РФ/ЕАЭС (с НДС 20 %), $ для остального мира (без налогов; в ЕС добавляется VAT по стране). Окончательные цены — раздел 16.
3.1 Прайс-лист
| Редакция | Модель | ₽ (ориентир) | $ (ориентир) | Примечания |
|---|---|---|---|---|
| Free | — | 0 | 0 | без ограничений по времени |
| Home | вечная (мажорная версия), 3 устройства | 2 990–3 990 | 39–49 | обновления в пределах 1.x + security-патчи 1.x бессрочно |
| Home | подписка, месяц | 199–249 | 2.99–3.49 | |
| Home | подписка, год | 1 990–2 490 | 29–34 | ≈ 2 месяца бесплатно |
| Pro | подписка, месяц | 499–699 | 7.99–9.99 | облако 100 ГБ, Assist 2000 запр./мес |
| Pro | подписка, год | 4 990–5 990 | 79–99 | |
| Pro | вечная без облака, 5 устройств | 7 990–9 990 | 99–129 | без cloud_*, assist_cloud,
support_priority остаётся |
| Business | за место, год | 5 990–7 990 | 89–119 | минимум 1 место, консоль, SSO, 1 ТБ/орг |
| Business | за место, месяц (предл.) | 649–799 | 9.99–12.99 | только с автопродлением |
| Enterprise | договор, за место, год | 9 000–15 000 | 129–199 | от 50 мест; on-prem сервер лицензий — включён; on-prem Cloud — +30 % |
| Enterprise | вечная объёмная (договор) | 2.5× годовой | 2.5× | + 20 %/год за обновления и поддержку (maintenance) |
| Education | за учащегося/сотрудника, год | 990–1 490 | 15–19 | верификация учебного заведения |
| Education | класс (30 мест), год | 19 900–24 900 | 299–349 | |
| Education | кампус (от 1000) | договор | договор | гранты — бесплатно по решению владельца |
| Плавающая (Enterprise) | за одновременный сеанс, год | 2× места | 2× | классическая пропорция 1 плавающая ≈ 2–3 именованных |
3.2 Скидки, апгрейды, обновления
| Политика | Правило |
|---|---|
| Объём (Business/Education) | 10–49 мест −10 %, 50–199 −15 %, 200–999 −20 %, 1000+ договор |
| Несколько лет вперёд | 2 года −10 %, 3 года −15 % |
| Апгрейд подписки Home → Pro | немедленно; доплата = (цена Pro − цена Home) × остаток периода / период; новый период не сбрасывается |
| Апгрейд вечной Home → вечная Pro | доплата = разница текущих цен −10 %; в течение 30 дней после покупки Home — ровно разница |
| Апгрейд Pro → Business | организация создаётся в портале; остаток Pro засчитывается в первый счёт |
| Даунгрейд | в конце оплаченного периода; лицензии перевыпускаются при следующей проверке |
| Мажорная версия для вечных | 1.x → 2.0: владельцам 1.x −40 % от цены 2.0 в первые 6 месяцев после выхода, затем −25 %; покупатели за 90 дней до выхода 2.0 получают 2.0 бесплатно |
update_until вечной |
security-обновления 1.x — бессрочно; функциональные 1.x — бессрочно; 2.x — только апгрейд |
| Возврат | 14 дней с момента покупки без объяснения причин (≤ 2 активаций); подписка — возврат первого платежа в течение 14 дней; далее — пропорциональный возврат только по закону страны; Enterprise — по договору |
| Региональные цены | ₽: РФ, Беларусь, Казахстан, Армения, Киргизия (цена ЕАЭС); €: ЕС/ЕЭЗ; $: остальные; PPP-коэффициенты 0.5–0.8 для Индии, Бразилии, Турции, Индонезии, Вьетнама (P1); регион фиксируется по стране биллинга и проверяется по карте/IP (антифрод) |
| Реселлеры | партии ключей со скидкой 20–35 % от прайса; минимальный заказ 10 ключей; возврат непроданных ключей из партии до активации — 180 дней |
| Промокоды | процентные/фиксированные/«+N месяцев»; на первый период или на весь срок; лимит использований; привязка к каналу |
3.3 Как цены живут в системе
- Сущность
products×prices(раздел 9): одна цена = (продукт, редакция, kind, период, валюта, регион, минимальное число мест). Прайс-лист — данные, не код. - Клиент никогда не показывает цену из своих ресурсов: Hub запрашивает
GET /v1/catalog?region=…у billing-service (кэш 24 ч), чтобы цена в приложении и в портале совпадала.
4. Лицензионный ключ
MRDN-…
4.1 Формат (зафиксировано)
MRDN-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX — префикс-литерал
MRDN и 25 символов Crockford Base32 (алфавит
0123456789ABCDEFGHJKMNPQRSTVWXYZ, без I L O U;
при вводе I/L → 1, O → 0, регистр и дефисы не
значимы). 25 × 5 = 125 бит, big-endian битовый
поток:
| Биты | Длина | Поле | Значение |
|---|---|---|---|
| 0–3 | 4 | version |
версия формата; сейчас 1 |
| 4–7 | 4 | edition |
1 free (резерв), 2 home, 3 pro, 4 business, 5 enterprise, 6 education, 7 trial; 8–15 резерв |
| 8–11 | 4 | kind |
1 perpetual, 2 subscription, 3 volume, 4 trial, 5 partner (NFR), 6 float_server; остальное резерв |
| 12–27 | 16 | seats |
число мест 1…65535 (семантика по kind, раздел 1) |
| 28–75 | 48 | serial |
batch (16 бит, партия) ‖ rnd (32 бита,
CSPRNG, уникален в партии) |
| 76–91 | 16 | issued |
дни от 2027-01-01 (0 — до эпохи/тест); хватает до 2206 г. |
| 92–119 | 28 | mac |
старшие 28 бит
HMAC-SHA256(K_issuer, "MRDN-KEY-v1" ‖ payload92) |
| 120–124 | 5 | check |
публичный контрольный символ (взвешенная сумма по модулю 32) |
Обоснование раскладки:
- Первые 24 символа = ровно 120 бит = payload (92) + mac (28);
25-й символ = check. Контрольный символ считается по символам,
а не по битам, поэтому любая одиночная опечатка обнаруживается
гарантированно (веса 1, 3, 5, …, 47 нечётны →
d·w ≢ 0 (mod 32)приd ≠ 0), перестановка соседних символов — в 31 случае из 32. Это даёт мгновенную подсказку «проверьте ключ» в поле ввода без обращения к серверу — и в офлайн-сценарии. - MAC 28 бит, а не 33: 5 бит отданы публичной
проверке; 28 бит (1/2.7·10⁸) достаточно, потому что ключ не
самодостаточен: сервер ищет
serialв БД. Вероятность, что случайный ключ одновременно пройдёт check, MAC и попадёт в выпущенныйserialпри 10 млн выпущенных ключей:(1/32)·(1/2²⁸)·(10⁷/2⁴⁸) ≈ 4·10⁻¹⁸; с лимитом 10 попыток/мин на IP перебор невозможен. MAC нужен, чтобы отсекать мусор до обращения к БД и чтобы подделанный ключ нельзя было «зарегистрировать» через утечку БД без секрета издателя. - Серийник 48 бит = партия 16 + случайные 32: партии нужны для реселлеров, розницы, акций и отзыва целыми партиями; 32 случайных бита в партии делают ключи неперечислимыми (нет «следующего» ключа).
- Секрет
K_issuer(32 байта) живёт только вtools/keygen(HSM/YubiKey) и в license-service (для быстрого отсева; в on-prem сервер не передаётся — он проверяет ключи только по своей БД выгрузки). Клиент секрета не знает и MAC не проверяет.
4.2 Кодирование и декодирование
// tools/keygen/src/key.rs — также используется в license-service и (без MAC) в meridian-license
pub const ALPHABET: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
pub const KEY_EPOCH: Date = date!(2027-01-01);
const DOMAIN: &[u8] = b"MRDN-KEY-v1";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct KeyFields { pub version: u8, pub edition: Edition, pub kind: KeyKind, pub seats: u16,
pub batch: u16, pub rnd: u32, pub issued_days: u16 }
impl KeyFields {
/// 92-битная полезная нагрузка / 92-bit payload (big-endian, младшие биты u128)
pub fn payload(&self) -> u128 {
(self.version as u128) << 88 | (self.edition as u128) << 84 | (self.kind as u128) << 80
| (self.seats as u128) << 64 | (self.batch as u128) << 48 | (self.rnd as u128) << 16
| self.issued_days as u128
}
}
/// MAC: HMAC-SHA256 над доменом и 12 байтами (92 бита + 4 нулевых) / 28 most significant bits
pub fn mac28(secret: &[u8; 32], payload: u128) -> u32 {
let bytes = (payload << 4).to_be_bytes(); // u128 -> 16 байт, значимы последние 12
let tag = hmac_sha256(secret, &[DOMAIN, &bytes[4..]].concat());
u32::from_be_bytes([tag[0], tag[1], tag[2], tag[3]]) >> 4
}
/// Контрольный символ по 24 символам / check symbol over the first 24 symbols
pub fn check_symbol(sym: &[u8; 24]) -> u8 {
let s: u32 = sym.iter().enumerate().map(|(i, c)| value(*c) as u32 * (2 * i as u32 + 1)).sum();
ALPHABET[(s % 32) as usize]
}
pub fn encode(f: &KeyFields, secret: &[u8; 32]) -> String {
let bits120: u128 = f.payload() << 28 | mac28(secret, f.payload()) as u128;
let mut sym = [0u8; 25];
for i in 0..24 { sym[i] = ALPHABET[((bits120 >> (5 * (23 - i))) & 31) as usize]; }
sym[24] = check_symbol(sym[..24].try_into().unwrap());
format!("MRDN-{}-{}-{}-{}-{}", s(&sym[0..5]), s(&sym[5..10]), s(&sym[10..15]), s(&sym[15..20]), s(&sym[20..25]))
}
/// Разбор без секрета: формат + check. Возвращает поля и mac28 для проверки на сервере.
/// Parse without the secret: format + check symbol; returns fields and mac28 for server-side verification.
pub fn decode(text: &str) -> Result<(KeyFields, u32), KeyError> {
let norm = normalize(text)?; // upper, drop '-', ' ', prefix; I/L->1, O->0
if norm.len() != 25 { return Err(KeyError::InvalidFormat); }
let sym: [u8; 25] = norm.as_bytes().try_into().unwrap();
if check_symbol(sym[..24].try_into().unwrap()) != sym[24] { return Err(KeyError::Checksum); }
let bits120 = sym[..24].iter().fold(0u128, |acc, c| acc << 5 | value(*c) as u128);
let payload = bits120 >> 28;
let f = KeyFields { version: (payload >> 88 & 15) as u8, edition: Edition::try_from((payload >> 84 & 15) as u8)?,
kind: KeyKind::try_from((payload >> 80 & 15) as u8)?, seats: (payload >> 64 & 0xFFFF) as u16,
batch: (payload >> 48 & 0xFFFF) as u16, rnd: (payload >> 16 & 0xFFFF_FFFF) as u32, issued_days: (payload & 0xFFFF) as u16 };
if f.version != 1 { return Err(KeyError::UnsupportedVersion(f.version)); }
Ok((f, (bits120 & 0x0FFF_FFFF) as u32))
}Отпечаток ключа для лицензии и журналов:
key_fingerprint = hex(SHA-256("MRDN-KEY-FP-v1" ‖ 25 символов))[..32].
Сам ключ в открытом виде на сервере хранится только в таблице
license_keys в зашифрованном поле (AES-256-GCM, ключ в KMS)
для показа владельцу в портале; поиск — по serial и по
отпечатку.
Примеры (MAC посчитан на тестовом секрете; полный
разбор — spec/license/license-key-format.md):
| Ключ | Разбор |
|---|---|
MRDN-28800-C0G3N-EQW3V-00JBK-T2E7W |
v1 · home · perpetual · 3 устройства · партия 0x0101 (розница) · выпуск 2027-03-15 |
MRDN-2CG00-M005N-MCBZE-007X7-KT6MP |
v1 · pro · subscription · 5 · партия 0x0002 (онлайн-магазин) · 2027-02-01 |
MRDN-2GG03-4011J-5C77N-01D1H-T6YY1 |
v1 · business · subscription · 25 мест · партия 0x0010 (реселлер) · 2027-06-30 |
MRDN-2MR1Y-G101X-KW9KV-G2XH2-NCQQN |
v1 · enterprise · volume · 500 активаций · партия 0x0200 · 2028-01-10 |
MRDN-2X000-47G12-BC1TB-00PJB-0JWWQ |
v1 · trial · trial · 1 · партия 0x0F00 (партнёрский trial) · 2027-04-01 |
4.3 Выпуск ключей:
tools/keygen
- Запускается только на защищённой машине владельца.
K_issuerхранится в YubiKey 5 (HMAC-SHA1 недоступен для SHA-256 → используется режим: YubiHSM 2 сhmac-sha256capability, либоK_issuerна смарт-карте под PIN, вычисление в памяти процесса без записи на диск). Экспорт секрета — только в сейф (Shamir 3 из 5). - Команды:
keygen batch new --edition pro --kind subscription --seats 5 --count 1000 --channel reseller:acme --note "...",keygen key show MRDN-…,keygen batch revoke 0x0010 --reason leak,keygen batch export 0x0010 --format csv|pdf-cards. - Журнал выпуска — append-only файл
issuance.log.jsonlс хэш-цепочкой (каждая запись содержит SHA-256 предыдущей) + копия в license-service (key_batches,license_keys); расхождение журнала и БД — алерт. - Партия (
key_batches):batch_id, редакция/тип/места,channel(direct, reseller:, retail: , promo, partner, internal), количество, статус draft → issued → live → closed | revoked, срок активации (activate_before— например, ключи акции), регион продаж, цена закупки реселлера, ответственный. - Ключ активируется только из партии в статусе
live: партия для розницы переводится вliveпри отгрузке (защита от «утёкших со склада» карточек). - Карточки-ключи для розницы: SKU на партию, ключ под
скретч-слоем, на карточке — QR с
https://account.meridian-office.<tld>/redeem?k=MRDN-…(ведёт в портал: регистрация/вход → привязка к аккаунту → скачивание), серийный номер карточки =batch:rndв hex для сверки без раскрытия ключа; печать — CSV с ключами в зашифрованном архиве для типографии (PGP-ключ типографии), тираж =count, остатки в партии видны в консоли владельца. - Партии для реселлеров: реселлер видит свои партии в кабинете реселлера (портал), выдаёт ключи покупателям, видит статус активации (без данных покупателя), может запросить отзыв/возврат неактивированного ключа.
5. Лицензионный файл
.mlic
5.1 Контейнер
Файл — CBOR (RFC 8949), одна карта верхнего уровня:
{
"v": 1, ; версия контейнера / container version
"payload": bstr(<CBOR payload>), ; полезная нагрузка — встроенная байтовая строка (как в COSE_Sign1),
; подписываются ровно эти байты — без повторной канонизации на клиенте
"signature": { "alg": "Ed25519", "key_id": "mo-lic-ce69679b", "sig": bstr(64) }
}
sig = Ed25519.sign(sk, "MRDN-LIC-v1" ‖ payload_bytes)
Payload сериализуется издателем детерминированно
(RFC 8949 §4.2, ключи — текстовые, отсортированы по длине и байтам),
чтобы одинаковые лицензии давали одинаковые байты (дедупликация,
журналы). Клиент только разбирает. Расширение .mlic, MIME
application/vnd.meridian.license, magic — первые байты
CBOR-карты A3 61 76 01. Для копирования вручную и e-mail
допускается текстовая форма:
-----BEGIN MERIDIAN LICENSE----- + base64 +
-----END …-----.
5.2 Поля payload
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
format_version |
int | да | версия схемы payload (1) |
license_id |
string | да | lic_ + ULID; уникален; используется в CRL и
/validate |
product |
string | да | meridian-office |
major_version |
int | да | мажорная версия, для которой действует лицензия (1) |
issuer |
string (URL) | да | базовый URL сервера, выдавшего лицензию
(https://license.meridian-office.ru, on-prem — свой) |
key_fingerprint |
string | нет | отпечаток ключа (нет у покупок через аккаунт и trial) |
edition |
enum | да | home … trial |
kind |
enum | да | perpetual, subscription,
volume, trial, partner,
float_lease |
features[] |
string[] | да | явный список флагов (snake_case), без ядра |
seats |
int | да | всего мест в праве (для показа «устройство 2 из 5») |
holder |
map | да | {account_id?} или {org_id?} +
display (имя/название, редактируемое) +
anonymous: bool |
device |
map | да* | {fingerprint_hash, components[3], name, os, os_version, arch, virtual, user_scope?};
отсутствует только в лицензии плавающего сервера |
issued_at |
tdate | да | момент выпуска (RFC 3339, UTC) |
valid_from |
tdate | да | начало действия |
expires_at |
tdate/null | да | конец действия; null — вечная |
grace_days |
int | да | льготный период после expires_at или после последней
успешной проверки (подписка: 30) |
update_channel |
enum | да | stable, beta, nightly,
lts — максимальный разрешённый канал |
update_until |
tdate/null | да | для вечных — конец права на функциональные обновления;
null — на весь срок действия |
limits |
map | да | числовые лимиты: devices,
cloud_quota_bytes, assist_requests_per_month,
collab_participants, … |
policy |
map | да | политики организации (раздел 8.7); для личных — {} |
validation |
map | да | {interval_days: 7, revocation_check_interval_days: 7, server_time_at_issue} |
revocation_check_interval |
int (сек) | да | дублирует интервал проверки CRL в секундах (для простых клиентов) |
float |
map | нет | только kind=float_lease:
{server_id, lease_id, lease_expires_at, offline_reserve_until, delegation} |
delegation |
map | нет | только лицензия on-prem сервера:
{delegate_pubkey, delegate_key_id, max_pool, may_issue: ["float_lease","volume"]} |
nonce |
bstr(16) | да | случайные байты (уникальность, защита от повторного выпуска) |
ext |
map | нет | расширения (игнорируются неизвестные) |
Пример (JSON-представление; подпись и key_id посчитаны
на тестовом ключе Ed25519, детерминированный CBOR payload — 1182 байта,
файл — 1334 байта):
{
"v": 1,
"payload": {
"format_version": 1,
"license_id": "lic_01J9ZK4M2XQ8R7S6T5V4W3Y2Z1",
"product": "meridian-office",
"major_version": 1,
"issuer": "https://license.meridian-office.ru",
"key_fingerprint": "136acb0924097e5febcc424907458f8c",
"edition": "pro",
"kind": "subscription",
"features": ["app_paint", "app_draw", "app_notes", "app_mail", "app_calendar", "app_base", "app_forms",
"pdf_edit", "pdf_forms", "pdf_sign", "pdf_convert", "scripts_run", "scripts_edit", "scripts_store",
"scripts_python", "cloud_storage", "cloud_collab", "cloud_share", "cloud_version_history",
"assist_local", "assist_cloud", "doc_sign", "doc_encrypt", "support_priority", "update_channel_beta"],
"seats": 5,
"holder": { "account_id": "usr_01J9ZJX0A1B2C3D4E5F6G7H8J9", "display": "Иван Петров", "anonymous": false },
"device": {
"fingerprint_hash": "0d9c1f3e9b2f7f1b6f0a3f6b4e1b9b6a0f2b7c8d9e1f2a3b4c5d6e7f8a9b0c1d",
"components": ["c1…64hex", "c2…64hex", "c3…64hex"],
"name": "IVAN-DESKTOP", "os": "windows", "os_version": "11.0.26100", "arch": "x86_64", "virtual": false
},
"issued_at": "2027-02-01T10:15:30Z",
"valid_from": "2027-02-01T00:00:00Z",
"expires_at": "2028-02-01T00:00:00Z",
"grace_days": 30,
"update_channel": "beta",
"update_until": null,
"limits": { "devices": 5, "cloud_quota_bytes": 107374182400, "assist_requests_per_month": 2000 },
"policy": {},
"validation": { "interval_days": 7, "revocation_check_interval_days": 7, "server_time_at_issue": "2027-02-01T10:15:30Z" },
"revocation_check_interval": 604800,
"nonce": "base64:7dX1k2Zq3n8w0Jk5Lm9pQg=="
},
"signature": {
"alg": "Ed25519",
"key_id": "mo-lic-ce69679b",
"sig": "Ze92SkziiShJsvvPq1R/0RobS3yOd1IvqIqYQ0Ozmn4BksVyY2fvZL0/JA6eSgxPrj3quVt5sZ9IhZ1zKwU6BQ=="
}
}Вечная Home без аккаунта отличается полями:
"kind":"perpetual", "expires_at":null,
"update_until":"2029-03-15T00:00:00Z",
"holder":{"anonymous":true,"display":"Ключ MRDN-28800-…-T2E7W"},
"validation":{"interval_days":30,…} (вечная лицензия
проверяется реже — только CRL и обновление политики),
"features" без
cloud_*/assist_*.
5.3 Проверка на клиенте (порядок)
load(path) → verify(now_local) → LicenseState
1. Разбор CBOR; v == 1; signature.alg == "Ed25519" иначе → Invalid(Malformed)
2. Публичный ключ по key_id: зашитые primary/reserve, затем делегированные (float) нет → Invalid(UnknownKey)
3. Ed25519.verify(pk, "MRDN-LIC-v1" ‖ payload_bytes, sig) нет → Invalid(BadSignature)
4. Разбор payload; format_version ∈ supported; product == "meridian-office"; major_version == app.major
нет → Invalid(WrongProduct|NewerFormat)
5. issuer ∈ {зашитый URL, URL из политики/конфига on-prem} нет → Invalid(UnknownIssuer)
6. Устройство: совпадение ≥ 2 из 3 components (раздел 6) нет → DeviceMismatch
7. CRL: license_id, key_fingerprint, batch, holder не отозваны есть → Revoked(reason)
8. Время: T = effective_now() (5.4); T < valid_from → NotYetValid
expires_at == null → Active (вечная; update_until отдельно влияет только на Update)
T ≤ expires_at → Active / Expiring (≤ 14 дн.)
expires_at < T ≤ expires_at + grace_days → GracePeriod (подписка: если нет сети — OfflineGrace)
иначе → Expired(ReadOnly)
9. Подписка: если T − last_successful_validation > validation.interval_days → запланировать /validate;
если > interval + grace_days и ни одной успешной проверки → OfflineGrace истёк → Expired(ReadOnly)
10. Features = from_names(features) ∪ core; применить limits/policy → LicenseState::Active { … }
Проверка выполняется: при старте каждого процесса приложения (Hub и каждый документ-процесс), при пробуждении ото сна, раз в час таймером, перед гейтируемыми операциями с побочным эффектом (сохранение в облако, запуск скрипта) — повторная проверка подписи из кэша в памяти (дёшево: Ed25519 verify ≈ 50 мкс).
5.4 Часы и защита от перевода времени
Клиент хранит в хранилище ОС (Keychain/DPAPI/Secret Service) запись
clock_anchor:
{ "server_time": "2027-05-02T09:00:00Z", "local_at_server_time": "2027-05-02T12:00:11+03:00",
"monotonic_at_server_time": 183400123, "boot_id": "…", "updated_at": "…" }server_timeобновляется при любом успешном ответе любого сервиса издателя (заголовокDateподписанного ответа/v1/timeили тело/validate) — не чаще раза в час.effective_now()=max(local_now, server_time + (monotonic_now − monotonic_at_server_time)), еслиboot_idтот же; после перезагрузки —max(local_now, server_time).- Если
local_now < local_at_server_time − 1 ч(часы переведены назад) →effective_nowберёт серверную ветвь; в UI — предупреждение «Системное время отстаёт от последней проверки». Документы не блокируются. - Если локальное время впереди серверного — используется локальное (невыгодно только пользователю).
- Вечные лицензии не зависят от часов, кроме
valid_from/update_until.
5.5 Хранение
| ОС | Пользовательская лицензия | Системная (объёмная/MDM, все пользователи) | Права |
|---|---|---|---|
| Windows | %LOCALAPPDATA%\Meridian Office\license\license.mlic |
%PROGRAMDATA%\Meridian Office\license\license.mlic |
пользовательская — ACL владельца; системная — запись только Administrators/SYSTEM, чтение всем |
| macOS | ~/Library/Application Support/Meridian Office/license/license.mlic |
/Library/Application Support/Meridian Office/license/license.mlic |
0600 / 0644 (владелец root) |
| Linux | $XDG_DATA_HOME/meridian-office/license/license.mlic
(Flatpak: внутри песочницы) |
/etc/meridian-office/license/license.mlic |
0600 / 0644 |
Приоритет: пользовательская > системная (пользователь с личным Pro
на корпоративной машине Home). Рядом — license.prev.mlic
(откат при неудачном обновлении), crl.cbor,
state.json (нечувствительное: даты последних проверок) —
чувствительные метки (clock_anchor,
trial.local, device_key) только в хранилище
ОС. Несколько лицензий (Home + Pro-trial, флаги объединяются по
максимуму) — папка licenses/*.mlic.
5.6 Ключи подписи и ротация
key_id=mo-lic-+ первые 8 hex SHA-256(raw pubkey). В приложение зашиты два ключа:primary(подписывает всё) иreserve(приватная часть — в сейфе, не в HSM «горячего» контура).- Ротация плановая (раз в 3 года) и аварийная (компрометация): новые
лицензии подписываются
reserve, клиент принимает обе; следующее обновление приложения приносит новую пару (reserve→primary, новыйreserve). Старые лицензии остаются валидными до перевыпуска (происходит незаметно при/validate). - Компрометация обоих — перевыпуск приложения; лицензии старого ключа
принимаются только до
update_untilтой сборки (минимальная версия клиента в CRL-манифесте). - On-prem сервер: получает от издателя
server.mlicсdelegation.delegate_pubkey(ключ сервера генерируется на сервере, публичная часть включается в заявку на лицензию сервера). Клиент принимаетfloat_lease, еслиsignature.key_id=delegate_key_id, а в комплекте с арендой приложенserver.mlic, подписанный издателем. Цепочка: издатель → сервер → аренда; глубина — 1.
5.7 Список отзыва (CRL)
crl.cbor = { "v":1, "payload": bstr{ "issued_at", "next_update", "seq", "min_client_version",
"revoked_licenses":[{id, reason, at}], "revoked_keys":[fingerprint…], "revoked_batches":[batch_id…],
"revoked_holders":[org_id…], "revoked_signing_keys":[key_id…] }, "signature":{…} }
- Доставка:
GET /v1/crl(ETag/If-None-Match) и как часть манифеста update-service (чтобы проверка обновлений уже приносила CRL — один сетевой контакт). Полный список (ожидаемо ≤ 1 МБ); дельты не нужны. - Клиент проверяет CRL не реже
revocation_check_interval; при недоступности — использует последний; CRL старше 90 дней → только предупреждение в Параметрах (документы не блокируются, подписка и так требует сети). - Причины:
leak,chargeback,fraud,transfer,superseded,admin,expired_batch.
6. Отпечаток устройства
6.1 Источники
| ОС | Компонент A (ОС) | Компонент B (плата/прошивка) | Компонент C (носитель) |
|---|---|---|---|
| Windows | HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid |
SMBIOS System UUID (Win32_ComputerSystemProduct.UUID,
через GetSystemFirmwareTable) |
серийный номер системного диска
(IOCTL_STORAGE_QUERY_PROPERTY, тома
%SystemDrive%) |
| macOS | IOPlatformUUID (IORegistry) |
серийный номер платформы IOPlatformSerialNumber |
UUID контейнера APFS системного тома
(diskutil info /) |
| Linux | /etc/machine-id (Flatpak:
/run/host/etc/machine-id, через портал) |
/sys/class/dmi/id/product_uuid (без root —
product_serial/board_serial недоступны;
fallback — product_uuid через systemd-id128?)
→ если недоступно: board_name+bios_vendor + MAC первого
несъёмного интерфейса |
серийник корневого блочного устройства
(/sys/block/<dev>/device/serial,
udev ID_SERIAL) или UUID корневой ФС |
Fallback-правила: если компонент недоступен (нет прав, виртуализация,
Flatpak), он заменяется на "unavailable" + подкомпонент
(например, MAC) и помечается в device.components_meta;
минимум 2 компонента обязательны, иначе активация идёт с флагом
weak_fingerprint (сервер ограничивает такие устройства: до
1 на право).
6.2 Нормализация и хэширование
// kernel/crates/meridian-license/src/fingerprint.rs
const SALT: &[u8] = b"meridian-device-fp-v1"; // публичная константа формата / public format constant
fn normalize(raw: &str) -> String {
raw.trim().to_ascii_uppercase().chars().filter(|c| c.is_ascii_alphanumeric()).collect() // без дефисов, пробелов, фигурных скобок
}
fn component_hash(kind: &str, raw: &str) -> [u8; 32] {
sha256(&[SALT, b"|", kind.as_bytes(), b"|", normalize(raw).as_bytes()].concat())
}
/// fingerprint_hash = SHA-256(SALT ‖ sorted(component hashes))
pub fn fingerprint(components: &[(&str, &str)]) -> Fingerprint {
let mut hashes: Vec<[u8; 32]> = components.iter().map(|(k, v)| component_hash(k, v)).collect();
hashes.sort();
Fingerprint { hash: sha256(&[SALT, &hashes.concat()].concat()), components: hashes }
}- Солёный SHA-256 → из отпечатка нельзя восстановить серийники;
«радужные» таблицы по MachineGuid бессмысленны из-за 128-битной энтропии
самого GUID, по серийникам дисков — затруднены солью с
kind. - Персональные данные не входят: ни имя пользователя, ни MAC в основной схеме (только как fallback).
6.3 Сравнение «2 из 3»
| Совпало компонентов | Решение клиента | Решение сервера при /validate или
/activate |
|---|---|---|
| 3 | лицензия действует | — |
| 2 | лицензия действует; состояние RebindPending |
перевыпускает .mlic с новыми компонентами, место
не расходуется; журнал device_rebound |
| 1 или 0 | DeviceMismatch (документы открыты, функции по
Free) |
считается новым устройством: нужна активация (расходует место) или автоперенос (раздел 7.8) |
Переустановка ОС на Windows меняет только MachineGuid → 2 из 3 →
место сохраняется. Замена диска → 2 из 3. Замена платы + диска → новое
устройство. Сервер хранит историю компонентов устройства
(devices.components_history), чтобы «склеивать» устройство
через две последовательные замены.
6.4 Виртуальные машины, терминальные серверы, контейнеры
- Детекция ВМ: CPUID hypervisor bit, DMI
sys_vendor/product_name(VMware, QEMU, VirtualBox, Microsoft Corporation- Virtual Machine, Parallels, Xen, KVM), macOS —
kern.hv_vmm_present. Результат →device.virtual = true.
- Virtual Machine, Parallels, Xen, KVM), macOS —
- Home/Pro на ВМ: разрешено, считается обычным устройством;
клонированная ВМ с тем же отпечатком — одна активация на всех клонах
(сервер видит несколько
/validateс разнымиinstance_id→ подсказка в портале, при > 5 — отказ в перевыпуске). - RDS / Citrix / VDI (non-persistent): отпечаток у
всех пользователей сервера одинаков → лицензирование по
пользователю: политика
policy.license_scope = "user"(Business/Enterprise);device.user_scope = SHA-256(SALT ‖ fingerprint_hash ‖ user SID/UID); каждая лицензия привязана к паре (устройство, пользователь); место = пользователь; хранение.mlicв профиле пользователя (roaming). Для non-persistent VDI с золотым образом — объёмный ключ сpolicy.auto_activate = trueиweak_fingerprintдопускается (активация при входе пользователя через Meridian ID/SSO, место — пользователь). - Контейнеры (Docker/LXC) — десктоп-клиент не поддерживается (нет ни
GUI, ни стабильного отпечатка); on-prem сервер лицензий живёт в
контейнере, его лицензия привязана не к железу, а к
org_id+ ключу сервера.
7. Потоки активации
Общие соглашения: клиент = meridian-license (крейт, без
UI) + диалоги Shell; все запросы — HTTPS, JSON, заголовки
X-Meridian-Client: write/1.2.0 (windows 11; x86_64; build 1.2.0.418),
Idempotency-Key: <uuid>; ответы об ошибках —
{ "error": { "code": "KEY_EXHAUSTED", "message": "…", "details": {…}, "retry_after": 0 } }
(полный список кодов — раздел 10.4 и
spec/license/api-license-service.md).
Машина состояний клиента (LicenseStatus):
NotActivated ──trial──▶ Trial ──expire──▶ Expired(ReadOnly)
│ ▲
├──activate/login──▶ Active ◀──renew──────┤
│ │ expires_at прошёл│
│ ├──▶ Grace ────────┘ (подписка: Grace = 30 дн. без сети/оплаты)
│ ├──▶ Revoked(reason) (из CRL или /validate)
│ ├──▶ DeviceMismatch (отпечаток < 2 из 3)
│ └──▶ RebindPending (2 из 3, перевыпуск при следующем контакте)
└──float lease──▶ Leased ──renew/15 мин──▶ Leased ──нет сервера 24 ч──▶ Expired(ReadOnly)
7.1 Онлайн-активация ключом
sequenceDiagram
autonumber
participant U as Пользователь
participant A as Приложение (Shell + meridian-license)
participant L as license-service
participant DB as PostgreSQL
participant H as HSM (Ed25519)
U->>A: Ввод ключа MRDN-… (AC-04)
A->>A: decode(): формат, check-символ → подсказка при опечатке
A->>A: fingerprint() + сбор device{name, os, arch, virtual}
A->>L: POST /v1/activate {key, device, client, account_token?}
L->>L: rate limit (IP, fingerprint) · mac28 на K_issuer
L->>DB: SELECT license_keys WHERE serial = … · партия live? не отозван? мест хватает?
alt ключ требует аккаунт (subscription, business)
L-->>A: 409 ACCOUNT_REQUIRED {login_url}
A->>U: Диалог входа (AC-01) → повтор с account_token
end
L->>DB: INSERT activation (entitlement, device, seat) · UPSERT device
L->>H: sign(payload)
H-->>L: sig
L->>DB: INSERT license (license_id, payload, sig)
L-->>A: 200 {license: base64(.mlic), seats_used: 1, seats_total: 3, server_time}
A->>A: verify() → сохранить license.mlic, clock_anchor, state
A-->>U: «Активировано: Home, устройство 1 из 3»
Ошибки и реакция:
| Код | HTTP | Причина | Что видит пользователь |
|---|---|---|---|
KEY_INVALID_FORMAT / KEY_CHECKSUM |
400 (или локально) | опечатка | «Проверьте ключ: символ 7 выглядит неверно» (позиция по check не вычисляется — подсветка всего поля) |
KEY_NOT_FOUND |
404 | нет в БД / партия не live / MAC не сошёлся | «Ключ не найден. Проверьте ввод или обратитесь к продавцу» |
KEY_REVOKED |
410 | партия/ключ отозваны | «Ключ отозван: причина. Поддержка: …» |
KEY_EXHAUSTED |
409 | все места заняты | «Все 3 устройства заняты» + список устройств + «Освободить» (если есть аккаунт/право управлять) |
KEY_EXPIRED |
410 | подписка по ключу истекла / activate_before прошёл |
«Срок действия ключа истёк» + «Продлить» |
KEY_ALREADY_BOUND |
409 | ключ привязан к другому аккаунту | «Ключ принадлежит другому аккаунту; войдите в него» |
KEY_REGION_MISMATCH |
403 | региональный ключ вне региона | «Ключ действует только в регионе …» |
ACCOUNT_REQUIRED |
409 | подписочный/организационный ключ | диалог входа, затем автоповтор |
CLIENT_TOO_OLD |
426 | клиент ниже min_client_version |
«Обновите приложение» |
RATE_LIMITED |
429 | лимит | «Повторите через N с» |
| сеть недоступна | — | — | «Нет связи с сервером. Повторить · Активировать офлайн (AC-05)» |
Состояние после успеха: Active; ключ сохраняется в
хранилище ОС (для показа и повторной активации);
seats_used/total — в state.json. Активация
идемпотентна по (key, fingerprint_hash):
повторный вызов возвращает ту же лицензию (перевыпущенную, если
изменились политики).
7.2 Активация через вход в Meridian ID
sequenceDiagram
autonumber
participant U as Пользователь
participant A as Приложение
participant ID as id-service
participant L as license-service
U->>A: «Войти» (AC-01)
A->>ID: OAuth 2.1 + PKCE, loopback http://127.0.0.1:<port>/callback (системный браузер)
ID-->>A: code → POST /oauth/token → access(15 мин) + refresh(90 дн., device-bound)
A->>A: токены → Keychain/DPAPI/Secret Service
A->>L: GET /v1/entitlements (Bearer access)
L-->>A: [{entitlement_id, edition: pro, kind: subscription, seats: 5, used: 2, expires_at, devices[]}]
alt одно подходящее право
A->>L: POST /v1/entitlements/{id}/activate {device, client}
else несколько (личное Pro + Business организации)
A->>U: выбор права (AC-06) — по умолчанию самое широкое
A->>L: POST /v1/entitlements/{id}/activate
end
L-->>A: 200 {license}
A-->>U: «Активировано: Pro (ivan@…), устройство 3 из 5»
- Покупка в портале создаёт
entitlementсholder.account_id; в приложении достаточно войти — ключ не нужен. - Ошибки:
DEVICE_LIMIT(409: список устройств + «Деактивировать …»),ENTITLEMENT_NOT_FOUND(404),ENTITLEMENT_EXPIRED(410, подписка не оплачена →SUBSCRIPTION_PAST_DUEс ссылкой на оплату),ORG_POLICY_DENIED(403, политика организации запрещает это устройство — например, ВМ или «только SSO»). - Привязка ключа к аккаунту: если пользователь вошёл и вводит ключ,
сервер создаёт право с
holder.account_idи помечает ключbound_account_id; все следующие активации — входом. Отвязать можно в портале (один раз в 90 дней).
7.3
Офлайн-активация (.mreq → портал/администратор →
.mlic)
sequenceDiagram
autonumber
participant U as Пользователь (изолированная машина)
participant A as Приложение
participant P as Портал / консоль администратора (другая машина)
participant L as license-service
U->>A: Ключ → «Активировать офлайн» (AC-05)
A->>A: собрать .mreq (ключ, device, client, nonce, requested_at) → файл + QR + текст base64
U->>P: Загрузить .mreq (или сканировать QR телефоном) на account.…/offline
P->>L: POST /v1/offline/request {mreq}
L->>L: те же проверки, что /activate · выпуск лицензии с validation.interval_days = 0 (офлайн)
L-->>P: .mlic (+ текст для копирования, + QR-серия для ввода через камеру — P2)
U->>A: Импорт license.mlic (файл / вставка текста)
A->>A: verify(): отпечаток совпадает (2 из 3) → Active
Формат .mreq (CBOR, та же обёртка
{v, payload: bstr, signature}; подпись — ключом устройства
device_key, Ed25519, который приложение генерирует при
первом запуске и хранит в хранилище ОС; публичный ключ входит в payload,
чтобы ответ можно было связать с запросом):
{ "v": 1, "payload": { "format_version": 1, "request_id": "req_01J9…", "kind": "activate",
"key": "MRDN-2MR1Y-G101X-KW9KV-G2XH2-NCQQN", "device": { "fingerprint_hash": "…", "components": ["…","…","…"],
"name": "WS-0412", "os": "windows", "os_version": "10.0.19045", "arch": "x86_64", "virtual": false },
"device_pubkey": "base64:…", "client": { "app": "hub", "version": "1.2.0", "build": "1.2.0.418" },
"requested_at": "2027-05-10T08:00:00Z", "nonce": "base64:…", "org_hint": "org_01J9…" },
"signature": { "alg": "Ed25519", "key_id": "dev", "sig": "base64:…" } }- Файл
activation-request-WS-0412.mreq;kind∈activate | deactivate | rebind; текстовая форма — base64 в-----BEGIN MERIDIAN REQUEST-----; QR — тот же base64 (≈ 700 байт, версия QR 20–25). - Офлайн-лицензия:
validation.interval_days = 0(не требует проверок),grace_daysне применяется, для подписокexpires_at= конец оплаченного периода + 14 дней (пересборка лицензии после продления — новым.mreqсkind=rebindили просто повторной загрузкой старого запроса в портал: сервер перевыпускает поrequest_id). - Enterprise: администратор загружает пачку
.mreq(zip) и получает zip.mlic(консоль, раздел 11.6); раскладка по машинам — скриптом (meridian-license-cli import --from \\share\licenses\%COMPUTERNAME%.mlic) или GPO. - Ошибки: те же коды, что
/activate, плюсREQUEST_EXPIRED(requested_at старше 30 дней),REQUEST_REPLAYED(nonce уже использован другим устройством),OFFLINE_NOT_ALLOWED(редакция безoffline_activation: Home/Pro — офлайн доступен только для вечных лицензий (решение: да, доступен — вечная Home не должна требовать сети навсегда)).
7.4 Плавающая лицензия (on-prem сервер)
sequenceDiagram
autonumber
participant A as Приложение (клиент в сети организации)
participant F as On-prem license-server (режим float)
participant Adm as Администратор
Note over F: server.mlic от издателя: пул 200, delegation
A->>F: POST /v1/float/lease {pool: "pro", device, user_scope, client}
alt есть свободное место
F-->>A: 200 {lease: .mlic kind=float_lease, lease_expires_at = +60 мин, offline_reserve_until = +24 ч, server_cert: server.mlic}
A->>A: verify(цепочка издатель→сервер) · Leased
else пул исчерпан
F-->>A: 409 FLOAT_POOL_EXHAUSTED {in_use: 200, pool: 200, queue_position: 3, retry_after: 30}
A->>A: режим ожидания (Free/ReadOnly), повтор каждые 30 с, уведомление пользователю
end
loop каждые 15 мин
A->>F: POST /v1/float/renew {lease_id}
F-->>A: 200 {lease_expires_at = now+60 мин} | 404 FLOAT_LEASE_NOT_FOUND (сервер перезапущен → новый lease)
end
Note over A: сервер недоступен: аренда действует до lease_expires_at, затем offline_reserve_until (24 ч) → Expired(ReadOnly)
A->>F: POST /v1/float/release {lease_id} (выход из всех приложений, сон > 15 мин → освобождение по таймауту)
Adm->>F: GET /v1/float/status → {pool, in_use, leases[{user, device, since, expires}], peak_24h, denied_24h}
Adm->>F: POST /v1/float/leases/{id}/revoke (принудительное освобождение)
A->>F: POST /v1/float/checkout {days: 10} (командировка: офлайн-резерв до 30 дней по политике → место занято на весь срок)
- Аренда — одна на (устройство, пользователь); все приложения одного пользователя делят аренду через Hub-брокер (Hub держит аренду, документ-процессы читают её из общей памяти/файла; при закрытии Hub — освобождение).
- Политики пула:
max_lease_minutes(60),renew_minutes(15),offline_reserve_hours(24),checkout_max_days(30),queue(вкл./выкл.),per_user_limit,deny_virtual. - Журнал сервера:
float_leases(кто, когда, сколько, с какого устройства, продления, отказы) + экспорт CSV; отчёт «пиковое использование по часам» для планирования закупки. - Срок аренды по умолчанию 60 мин выбран так, чтобы потеря сервера не выбрасывала пользователя посреди работы, а зависшие клиенты освобождали места за час; 24 ч резерв — чтобы ноутбук, ушедший домой, проработал до утра.
7.5 Объёмная активация Enterprise (MAK-подобный ключ)
sequenceDiagram
autonumber
participant M as MDM/GPO/конфиг-файл
participant A as Приложение (первый запуск на рабочей станции)
participant Px as Прокси организации
participant L as license-service (облако или on-prem)
M->>A: deploy.json: {key: "MRDN-2MR1Y-…", server: "https://license.corp.local", proxy: "http://proxy:3128", auto_activate: true, name_template: "${HOSTNAME}"}
A->>A: конфиг найден → активация без диалога (пользователь видит только значок)
A->>Px: CONNECT license.corp.local:443 (системный или явный прокси, NTLM/Kerberos/basic из конфига или системы)
Px->>L: POST /v1/activate {key, device, client, org_hint}
L-->>A: 200 {license} (seats_used 213/500)
Note over A,L: при 500/500 → KEY_EXHAUSTED → администратору в консоль уведомление, клиент повторяет раз в сутки, работает как Free
- Места расположения
deploy.json(поиск по порядку, первый найденный — побеждает): Windows —HKLM\SOFTWARE\Policies\Meridian Office(ADMX-шаблонMeridianOffice.admx: ключ, сервер, прокси, канал обновлений, политики),%PROGRAMDATA%\Meridian Office\deploy.json; macOS — профиль MDMcom.meridian-office.deploy(plist),/Library/Application Support/Meridian Office/deploy.json; Linux —/etc/meridian-office/deploy.json,/etc/meridian-office/deploy.d/*.json. Параметры также передаются установщику:msiexec … LICENSE_KEY=… LICENSE_SERVER=…. - Ключ в конфиге хранится открыто (это объёмный ключ, защищается
отзывом +
activate_before+ привязкой к IP-диапазонам организацииpolicy.allowed_cidrsи/или к on-prem серверу). - Без интернета:
serverуказывает на on-prem license-server, который имеет свою выгрузку ключей организации (server.mlicсdelegation.may_issue: ["volume"], пул = число активаций); совсем без сервера — офлайн-пачка (7.3). - Переактивация после смены железа — автоматически (2 из 3); при превышении активаций администратор освобождает устройства в консоли (неактивные > 90 дней — кнопка «Освободить неактивные»).
7.6 Деактивация и перенос устройства
sequenceDiagram
autonumber
participant U as Пользователь
participant A as Приложение (старое устройство)
participant P as Портал
participant L as license-service
participant B as Приложение (новое устройство)
alt из приложения (есть сеть)
U->>A: Параметры → Аккаунт и лицензия → «Деактивировать это устройство»
A->>L: POST /v1/deactivate {license_id, device, reason: "user"}
L-->>A: 200 {seats_used: 2, seats_total: 3}
A->>A: удалить license.mlic → NotActivated (Free)
else из портала (устройство утеряно/нет сети)
U->>P: Устройства → «Деактивировать» IVAN-OLD
P->>L: POST /v1/devices/{id}/deactivate {reason: "lost"}
L->>L: место освобождено · license_id → CRL (reason: transfer)
Note over A: при следующем контакте старое устройство получает Revoked(transfer) → Free
else офлайн-деактивация
U->>A: «Деактивировать офлайн» → файл .mreq kind=deactivate, лицензия удаляется локально
U->>P: Загрузить .mreq → место освобождено
end
U->>B: Активация ключом/входом → Active
- Лимиты самообслуживания (антизлоупотребление): для личных прав — не
более 3 деактиваций за 30 дней на право; сверх — обращение в поддержку
(
TRANSFER_LIMIT, 429 с датой следующей возможности). Для организаций — без лимита, но с журналом. - Деактивация из портала без сети на старом устройстве — доверяем
пользователю; старое устройство продолжит работать до истечения
grace_days(подписка) или до первого контакта с сервером (вечная — пока не получит CRL). Для вечных без сети это теоретическая дыра «две машины на одно место» — принимаем (P1; по 2 активации на место при 3 переносах за 90 дней — честных не затрагивает).
7.7 Проверка подписки, истечение, льготный период
sequenceDiagram
autonumber
participant A as Приложение
participant L as license-service
Note over A: при старте: now − last_validation ≥ 7 дн.? → фоновая проверка (не блокирует UI)
A->>L: POST /v1/validate {license_id, device, client, state_hash}
alt всё в порядке
L-->>A: 200 {status: "ok", server_time, license?: (новая .mlic, если изменились срок/функции/политики), crl_seq}
A->>A: last_validation = now · заменить license.mlic при наличии новой (атомарно, prev — для отката)
else подписка не оплачена
L-->>A: 200 {status: "past_due", pay_url, grace_until}
A->>A: баннер «Подписка не оплачена, N дн. до режима чтения»
else отозвано / устройство удалено
L-->>A: 200 {status: "revoked", reason}
A->>A: Revoked → Free + ReadOnly для Pro-приложений
else сеть недоступна
A->>A: повтор по расписанию 1 ч, 6 ч, 24 ч · состояние OfflineGrace с day = now − last_validation − 7
end
Таймлайн подписки (пример: оплата не прошла 1 марта):
| Дата | Биллинг (раздел 9) | Клиент |
|---|---|---|
| T−7 дн. | письмо «скоро списание» | — |
| T (1 марта) | попытка списания неуспешна → past_due; письмо 1 |
/validate → past_due; баннер жёлтый |
| T+3, T+7 | повторные списания; письма 2, 3 | баннер с датой |
| T+14 | подписка suspended; право expires_at =
T+14 |
/validate → expired; клиент входит в
«мягкий» режим на 7 дней (красный баннер, всё работает), чтобы не ломать
рабочий день; льготные 30 дней (grace_days) действуют
только при отсутствии сети |
| T+21 | — | Expired(ReadOnly) |
| T+14…T+90 | облако только чтение; письмо о переходе в архив | — |
| T+90 | данные облака удаляются (после 3 писем) | — |
Правило льготного периода без сети: последняя успешная проверка
V; до V + 7 дн. — норма;
V + 7 … V + 37 — OfflineGrace(n) с
ненавязчивым индикатором (не баннер, а значок в строке состояния Hub;
баннер — с 7 дней до конца); после — Expired(ReadOnly) до
первого успешного /validate. Вечные лицензии в этот режим
не попадают.
7.8 Отзыв (ключа, лицензии, партии)
sequenceDiagram
autonumber
participant O as Владелец / поддержка (консоль издателя)
participant L as license-service
participant Up as update-service
participant A as Приложение
O->>L: POST /internal/v1/keys/revoke {batch: 0x0010, reason: "leak"} | /licenses/{id}/revoke | /holders/{org}/revoke
L->>L: статус revoked · пересборка crl.cbor (seq+1), подпись в HSM · события в audit_log
L->>Up: publish crl.cbor (манифест обновлений несёт seq)
A->>Up: проверка обновлений (ежедневно) → crl seq > local → GET crl.cbor
A->>A: license_id/key_fingerprint/batch в CRL → Revoked(reason)
Note over A: Revoked: Free-ядро работает, Pro-приложения ReadOnly · сообщение с причиной и контактами
A->>L: следующий /validate → подтверждение revoked (или перевыпуск, если отзыв снят: reason superseded)
- Отзыв партии при утечке (например, ключи реселлера выложены в сеть):
честным покупателям этой партии сервер автоматически
перевыпускает право на новый ключ при
/validate(status: "reissued", новый ключ в портале и в ответе), если устройство активировало ключ до даты утечки и не превысило места — «честным удобно». - Отзыв по chargeback — без перевыпуска; по
fraud— блокировка аккаунта.
7.9 Смена редакции (апгрейд/даунгрейд)
sequenceDiagram
autonumber
participant U as Пользователь
participant A as Приложение
participant Bl as billing-service
participant L as license-service
U->>A: «Замок» на функции → «Перейти на Pro» (AC-08)
A->>A: открыть портал в браузере: account.…/upgrade?from=home&to=pro&device=…&src=app_paint
U->>Bl: оплата (ЮKassa/CloudPayments/Stripe)
Bl->>L: event subscription.updated {entitlement_id, edition: pro, expires_at}
L->>L: entitlement изменён · все активные лицензии права помечены stale (нужен перевыпуск)
A->>L: POST /v1/validate (клиент опрашивает каждые 30 с, пока открыт портал апгрейда, затем — обычное расписание)
L-->>A: 200 {status: "ok", license: новая (edition pro, features+)}
A->>A: заменить license.mlic · событие LicenseChanged → все окна снимают замки без перезапуска
- Даунгрейд (Pro → Home, конец оплаченного периода): новая лицензия с меньшим набором; документы Pro-функций открываются (round-trip), приложения вне Home — ReadOnly; облако — 90 дней на скачивание (раздел 2.4).
- Апгрейд устройств (Home 3 → Pro 5) — мгновенно; даунгрейд при
превышении (Pro 5 устройств → Home 3): лицензии остаются у трёх
последних активных устройств, остальные получают
DEVICE_LIMITпри проверке + письмо.
7.10 Восстановление после переустановки ОС / новой машины
sequenceDiagram
autonumber
participant U as Пользователь
participant A as Приложение (после переустановки)
participant L as license-service
U->>A: Вход в аккаунт или ввод ключа (как при первой активации)
A->>L: POST /v1/activate | /entitlements/{id}/activate {device(components новые)}
L->>L: поиск устройства права с совпадением ≥ 2 компонентов
alt найдено (переустановка ОС: MachineGuid новый, UUID и диск прежние)
L->>L: device.components обновлены · та же activation · место не расходуется
L-->>A: 200 {license (перевыпущена), reused: true}
else не найдено, но мест нет
L->>L: автоперенос: если за 30 дней не было автопереносов по праву и есть устройство без контакта > 30 дн. → освободить его
L-->>A: 200 {license, transferred_from: "IVAN-OLD"} | 409 DEVICE_LIMIT {devices[]}
end
- Вечная Home без аккаунта: ключ хранится у пользователя (письмо о
покупке, карточка, портал); повторный ввод ключа на той же машине →
reused: true. Потерянный ключ без аккаунта восстановить нельзя — при покупке без аккаунта письмо содержит ключ и предложение «привязать к аккаунту, чтобы не потерять».
8. Meridian ID
8.1 Модель данных
users 1──* credentials (password | totp | webauthn | recovery_codes)
users 1──* oauth_identities (google | apple | microsoft | yandex | vk | oidc:<org>)
users 1──* sessions 1──* refresh_tokens
users 1──* devices (отпечаток + device_pubkey; общие с license-service через device_id)
users 1──* consents
users *──* organizations через org_members(role)
organizations 1──* groups *──* users (group_members)
organizations 1──* policies (версии; одна активная)
organizations 1──* sso_connections (oidc | saml)
organizations 1──* api_keys (scope: scim | admin | license-read)
users/organizations 1──* audit_log
magic_links, password_resets, email_verifications — одноразовые токены (хэш, TTL)
DDL основных таблиц (полный —
spec/license/db-schema.sql; здесь — ключевые столбцы):
CREATE TABLE users (
id TEXT PRIMARY KEY, -- usr_<ULID>
email CITEXT NOT NULL UNIQUE,
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
display_name TEXT NOT NULL DEFAULT '',
locale TEXT NOT NULL DEFAULT 'ru-RU',
region TEXT NOT NULL CHECK (region IN ('ru','eu')), -- кластер хранения / data region
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active','locked','deleted','pending_deletion')),
mfa_required BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ
);
CREATE TABLE credentials (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
kind TEXT NOT NULL CHECK (kind IN ('password','totp','webauthn','recovery')),
secret BYTEA NOT NULL, -- argon2id PHC-строка | TOTP seed (AES-GCM, KMS) | COSE pubkey | хэши кодов
label TEXT, -- «YubiKey 5C», «iPhone»
counter BIGINT NOT NULL DEFAULT 0, -- WebAuthn sign count
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
last_used_at TIMESTAMPTZ
);
CREATE TABLE sessions (
id TEXT PRIMARY KEY, -- ses_<ULID>, попадает в JWT как sid
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
device_id TEXT REFERENCES devices(id),
client TEXT NOT NULL, -- 'desktop' | 'portal' | 'admin' | 'api'
ip INET, user_agent TEXT,
auth_level SMALLINT NOT NULL DEFAULT 1, -- 1 пароль/OAuth, 2 + второй фактор, 3 passkey
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ NOT NULL, -- 90 дней скользящих
revoked_at TIMESTAMPTZ, revoked_reason TEXT
);
CREATE TABLE refresh_tokens (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
family_id TEXT NOT NULL, -- цепочка ротации / rotation family
token_hash BYTEA NOT NULL UNIQUE, -- SHA-256(token)
parent_id TEXT REFERENCES refresh_tokens(id),
device_pubkey BYTEA, -- привязка к ключу устройства / device binding
issued_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ NOT NULL,
used_at TIMESTAMPTZ -- заполняется при ротации; повторное использование → отзыв семьи
);
CREATE TABLE organizations (
id TEXT PRIMARY KEY, -- org_<ULID>
name TEXT NOT NULL, slug CITEXT UNIQUE,
region TEXT NOT NULL CHECK (region IN ('ru','eu')),
kind TEXT NOT NULL DEFAULT 'business' CHECK (kind IN ('business','enterprise','education','reseller')),
billing_customer_id TEXT, -- ссылка в billing-service
sso_required BOOLEAN NOT NULL DEFAULT FALSE,
verified_domains TEXT[] NOT NULL DEFAULT '{}',
status TEXT NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE org_members (
org_id TEXT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role TEXT NOT NULL CHECK (role IN ('owner','admin','billing_admin','license_admin','auditor','member','guest')),
source TEXT NOT NULL DEFAULT 'manual' CHECK (source IN ('manual','scim','sso_jit','invite')),
external_id TEXT, -- SCIM externalId
joined_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (org_id, user_id)
);
CREATE TABLE policies (
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
version INTEGER NOT NULL,
body JSONB NOT NULL, -- раздел 8.7 / section 8.7
is_active BOOLEAN NOT NULL DEFAULT FALSE,
created_by TEXT REFERENCES users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (org_id, version)
);
CREATE TABLE sso_connections (
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
protocol TEXT NOT NULL CHECK (protocol IN ('oidc','saml')),
config JSONB NOT NULL, -- issuer, client_id, metadata_url | idp_entity_id, x509, acs
client_secret BYTEA, -- AES-GCM (KMS)
domains TEXT[] NOT NULL, -- маршрутизация по домену e-mail
jit_provisioning BOOLEAN NOT NULL DEFAULT TRUE,
enabled BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE TABLE api_keys (
id TEXT PRIMARY KEY, org_id TEXT REFERENCES organizations(id) ON DELETE CASCADE,
name TEXT NOT NULL, key_hash BYTEA NOT NULL UNIQUE, prefix TEXT NOT NULL, -- 'mok_live_ab12…'
scopes TEXT[] NOT NULL, -- scim, org:read, licenses:read, licenses:manage
expires_at TIMESTAMPTZ, last_used_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE audit_log (
id BIGSERIAL PRIMARY KEY,
at TIMESTAMPTZ NOT NULL DEFAULT now(),
actor_type TEXT NOT NULL CHECK (actor_type IN ('user','api_key','system','support')),
actor_id TEXT, org_id TEXT, target_type TEXT, target_id TEXT,
action TEXT NOT NULL, -- 'user.login', 'license.activate', 'policy.update', …
ip INET, user_agent TEXT,
details JSONB NOT NULL DEFAULT '{}',
prev_hash BYTEA, row_hash BYTEA NOT NULL -- хэш-цепочка / hash chain
);
CREATE TABLE consents (
id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
kind TEXT NOT NULL, -- 'pdn_152fz', 'privacy_policy', 'eula', 'marketing', 'telemetry'
version TEXT NOT NULL, granted BOOLEAN NOT NULL, at TIMESTAMPTZ NOT NULL DEFAULT now(),
ip INET, source TEXT NOT NULL -- 'portal' | 'app' | 'checkout'
);8.2 Аутентификация
| Способ | Реализация |
|---|---|
| Пароль | Argon2id, m = 64 MiB, t = 3, p = 1,
соль 16 байт, тег 32 байта, PHC-строка; pepper (32 байта в KMS/HSM)
применяется как HMAC-SHA256(pepper, password) перед Argon2
— утечка БД без pepper бесполезна. Политика: ≥ 10 символов, без
обязательных классов, проверка по HIBP k-anonymity (без отправки
пароля), запрет e-mail в пароле. Перехэширование при входе, если
параметры устарели. Лимит: 10 неуспешных попыток/15 мин на аккаунт →
задержка 2ⁿ с + капча портала; 100/час с IP |
| Ссылка-вход (magic link) | токен 32 байта CSPRNG, хранится SHA-256, TTL 15 мин, одноразовый,
привязан к client_nonce из запроса (ссылка, открытая в
другом браузере, требует подтверждения кода из 6 цифр — защита от
перехвата почты) |
| OAuth-провайдеры | Google (OIDC), Apple (Sign in with Apple, с relay-email), Microsoft
(Entra ID, personal + work), Яндекс ID (OAuth 2.0), VK ID (OAuth 2.1).
Связывание с существующим аккаунтом только после подтверждения e-mail;
oauth_identities(provider, subject) UNIQUE |
| Корпоративный SSO | OIDC (Authorization Code + PKCE, discovery) и SAML 2.0
(SP-initiated, HTTP-POST binding, подписанные assertions, шифрование
опционально); маршрутизация по домену e-mail →
sso_connections.domains; JIT-провиженинг участника с ролью
member; sso_required запрещает пароль/OAuth
для доменов организации |
| 2FA | TOTP (RFC 6238, SHA-1 для совместимости, 30 с, окно ±1),
passkeys/WebAuthn (резидентные ключи,
userVerification=preferred, attestation none),
10 одноразовых кодов восстановления (хэши). Обязательна для ролей
owner/admin/billing_admin/license_admin и по политике организации |
| Восстановление | сброс пароля по почте (TTL 1 ч, одноразово, инвалидирует все сессии при смене), коды восстановления 2FA; потеря всего — ручная верификация поддержкой (платёжные данные, дата регистрации) с задержкой 72 ч и письмом на все известные адреса |
| Подтверждение e-mail | обязательно для покупок и организаций; без подтверждения — только Free/Trial |
8.3 Сессии и токены
// access JWT (EdDSA/Ed25519, kid в заголовке), TTL 15 мин
{ "iss": "https://id.meridian-office.ru", "sub": "usr_01J9…", "sid": "ses_01J9…", "did": "dev_01J9…",
"aud": ["license", "cloud", "billing"], "scope": "profile licenses cloud",
"org": "org_01J9…", "roles": ["member"], "amr": ["pwd", "totp"], "acr": "2", "region": "ru",
"iat": 1780000000, "exp": 1780000900, "jti": "…" }- Refresh 90 дней, непрозрачная строка 32 байта
(
mor_…), в БД — хэш; ротация при каждом использовании: новый токен сparent_id, старый помечаетсяused_at. Повторное предъявление уже использованного токена → отзыв всей семьи (family_id) и сессии, письмо пользователю, запись в аудит. - Device-bound: при входе с десктопа клиент передаёт
device_pubkey(Ed25519 из хранилища ОС); запрос/oauth/tokenсgrant_type=refresh_tokenподписывается этим ключом (заголовокX-Meridian-Device-Proof: base64(sig(ts ‖ token_hash))); украденный refresh без ключа устройства бесполезен. Для портала (браузер) — cookie__Host-, SameSite=Lax, без device-proof. - Сессия — скользящая 90 дней; абсолютный максимум 1 год; выход «со всех устройств» = отзыв сессий.
- JWT проверяются сервисами локально по JWKS
(
/.well-known/jwks.json, ротация ключей раз в 90 дней, два активных). Отзыв access до истечения — через короткий TTL + список отозванныхsidв Redis (проверяется cloud-service и portal).
8.4 SCIM 2.0
- Эндпоинты
/scim/v2/Users,/scim/v2/Groups,/scim/v2/ServiceProviderConfig,/scim/v2/Schemas; Bearer —api_keysсо scopescim; фильтрыuserName eq "…",externalId eq "…"; пагинацияstartIndex/count. - Маппинг:
userName→ email,name.formatted→ display_name,active=false→ отзыв мест и сессий,groups→group_members; удаление пользователя SCIM →org_membersудаляется, личный аккаунт остаётся (данные организации переходят владельцу по политике). - Поддерживаемые IdP: Entra ID, Okta, Google Workspace, Keycloak, Яндекс 360 (через OIDC без SCIM — JIT).
8.5 Роли и права (RBAC организации)
| Действие | owner | admin | billing_admin | license_admin | auditor | member | guest |
|---|---|---|---|---|---|---|---|
| Управлять владельцами, удалить организацию | ✓ | — | — | — | — | — | — |
| Участники, группы, роли (кроме owner) | ✓ | ✓ | — | — | — | — | — |
| SSO, SCIM, домены, API-ключи | ✓ | ✓ | — | — | — | — | — |
| Политики | ✓ | ✓ | — | — | — | — | — |
| Подписки, счета, способы оплаты, реквизиты | ✓ | ✓ | ✓ | — | — | — | — |
| Назначать/отзывать места, ключи, устройства, офлайн-пакеты, плавающий сервер | ✓ | ✓ | — | ✓ | — | — | — |
| Читать журнал аудита, отчёты | ✓ | ✓ | ✓ (биллинг) | ✓ (лицензии) | ✓ | — | — |
| Пользоваться назначенным местом, облаком организации | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — |
| Доступ к документам, которыми поделились (без места) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
Права вычисляются в id-service (permissions в
access-токене не передаются — сервисы запрашивают
GET /v1/orgs/{id}/members/{uid}/permissions с кэшем 60 с),
чтобы изменение роли действовало сразу.
8.6 Устройства
devices: id (dev_ULID),
user_id, fingerprint_hash,
components[3], components_history JSONB,
device_pubkey, name, os,
os_version, arch, virtual,
weak_fingerprint, first_seen_at,
last_seen_at, app_versions JSONB,
revoked_at. Пользователь видит и переименовывает устройства
в портале и в AC-12; «удалить устройство» = отзыв сессий + деактивация
лицензий на нём.
8.7 Политики организации
{
"version": 3,
"cloud": "allow", // allow | deny | onprem_only
"cloud_endpoint": "https://cloud.corp.local",
"sso_required": true,
"mfa_required_roles": ["owner","admin","license_admin","billing_admin"],
"update_channel_max": "stable", // stable | lts | beta | nightly (клиент не может выбрать выше)
"update_defer_days": 14,
"scripts": { "run": "signed_only", "allowed_publishers": ["meridian", "org:org_01J9…"], "store": false, "python": false },
"assist": "local_only", // deny | local_only | cloud
"telemetry": "crash_only", // off | crash_only | full
"license_scope": "device", // device | user (RDS/VDI → user)
"device_limit_per_user": 3,
"deny_virtual_devices": false,
"offline_grace_days": 30,
"allowed_cidrs": ["10.0.0.0/8"], // активации объёмного ключа только отсюда
"data_region": "ru",
"doc_defaults": { "require_signature_for_scripts": true, "default_format": "mop" }
}Доставка: политика попадает в .mlic
(policy{}) при выпуске/перевыпуске (/validate
приносит новую версию); локальные источники
(GPO/MDM/deploy.json) объединяются с серверной по правилу
«самое строгое побеждает» для запретов и «локальная побеждает» для
адресов серверов. Клиент применяет политику через
Policy::effective() в meridian-license.
8.8 Регионы хранения
| Регион | Где | Для кого | Что локализовано |
|---|---|---|---|
ru |
ЦОД в РФ (два, активный-резервный) | пользователи с биллинг-страной РФ и организации РФ | users, credentials, sessions, devices, consents, audit, billing (ПДн граждан РФ — первично в РФ, 152-ФЗ) |
eu |
ЕС (Франкфурт/Амстердам) | остальной мир | то же под GDPR |
- Регион выбирается при регистрации по стране (IP + явный выбор) и фиксируется; смена — миграция по заявке.
- Глобальный directory (минимальный):
email_hash → region,org_slug → region— только для маршрутизации входа; общие справочники (products, prices, CRL, JWKS) реплицируются из «мастер»-региона издателя. - license-service — по регионам (лицензии и устройства живут рядом с
пользователем);
issuerв лицензии указывает региональный URL; клиент не знает о регионах — после входа получаетissuerиз профиля.
8.9 Удаление и экспорт данных
- Экспорт (
POST /v1/me/export): архив JSON (профиль, устройства, права, история покупок, согласия, журнал входов)- ссылка на архив облака; готовится ≤ 72 ч, ссылка 7 дней.
- Удаление (
POST /v1/me/delete): подтверждение паролем/2FA, период отмены 14 дней (pending_deletion), затем: личные данные обезличиваются (e-mail → хэш, имя → «Удалённый пользователь»), лицензии отзываются, облако удаляется; бухгалтерские документы хранятся обезличенно 5 лет (требование ФЗ-402/НК), аудит организации — не удаляется (актор →deleted_user). - Владелец организации не может удалить аккаунт, не передав владение.
8.10 Журнал аудита
События: user.login{method},
user.login_failed, user.mfa_enrolled,
session.revoked, token.reuse_detected,
org.member_added/removed/role_changed,
org.policy_updated{version}, org.sso_updated,
license.activated/deactivated/ revoked/reissued,
seat.assigned/unassigned, float.lease_denied,
billing.invoice_paid/refunded, data.exported,
account.deleted. Хранение: личный аккаунт — 1 год, Business
— 1 год, Enterprise — 3 года; выгрузка CSV/JSON и стриминг в SIEM
(webhook/HTTPS, формат JSON Lines) — Enterprise. Хэш-цепочка
prev_hash/row_hash + ежедневный якорь (корень дня) в
отдельном хранилище для доказуемости.
9. Биллинг
9.1 Сущности
products 1──* prices (редакция × kind × период × валюта × регион × min_seats)
customers (физ/юр, регион, налоговый статус, реквизиты) 1──* payment_methods (токены провайдера)
customers 1──* orders 1──* order_items ──▶ entitlements (license-service)
customers 1──* subscriptions (price, seats, period, status, next_billing_at) 1──* invoices 1──* invoice_lines
invoices 1──* payments 1──* refunds; payments 1──* fiscal_receipts (54-ФЗ)
promo_codes 1──* promo_redemptions
resellers 1──* reseller_orders ──▶ key_batches (license-service)
webhook_events (provider, event_id UNIQUE, payload, processed_at) -- идемпотентность вебхуков
edu_verifications (org, документ, статус, срок)
tax_rates (страна, регион, ставка, тип: vat_ru | vat_eu | sales_tax | none)
Статусы подписки:
trialing → active → past_due → suspended → canceled | expired;
active → canceled_at_period_end. Статусы счёта:
draft → open → paid | void | uncollectible. Платёж:
pending → succeeded | failed | canceled; возврат:
pending → succeeded | failed.
9.2 Провайдеры платежей
| Провайдер | Регион | Что используем | Фискализация |
|---|---|---|---|
| ЮKassa | РФ | карты (включая МИР), СБП, ЮMoney, SberPay, автоплатежи (сохранённый
способ), виджет/редирект, вебхуки payment.succeeded,
payment.canceled, refund.succeeded |
чек 54-ФЗ через ЮKassa (параметр receipt в запросе:
позиции, vat_code, e-mail/телефон покупателя,
payment_subject: service,
payment_mode: full_payment) |
| CloudPayments | РФ (резерв/второй), ЕАЭС | карты, рекуррентные списания по токену, Apple/Google Pay (где
доступно), вебхуки Pay, Fail,
Refund, Recurrent |
чеки через встроенную онлайн-кассу (CloudKassir) или своя ККТ через API |
| Stripe | остальной мир | Checkout + Billing (подписки, прорация, дэнинг), Tax (VAT/sales
tax), Customer Portal; вебхуки invoice.paid,
invoice.payment_failed,
customer.subscription.updated/deleted,
charge.refunded, charge.dispute.created |
не требуется (чеки Stripe) |
Адаптеры реализуют один trait:
// services/billing-service/src/providers/mod.rs
#[async_trait]
pub trait PaymentProvider: Send + Sync {
fn id(&self) -> ProviderId; // yookassa | cloudpayments | stripe
async fn create_checkout(&self, req: CheckoutRequest) -> Result<CheckoutSession>; // redirect_url, provider_ref
async fn charge_saved(&self, pm: &PaymentMethod, amount: Money, meta: ChargeMeta) -> Result<PaymentResult>;
async fn refund(&self, payment: &Payment, amount: Money, reason: &str) -> Result<RefundResult>;
fn verify_webhook(&self, headers: &HeaderMap, body: &[u8]) -> Result<WebhookEvent>; // подпись/IP-allowlist
async fn fiscalize(&self, payment: &Payment, receipt: Receipt) -> Result<FiscalReceipt>; // no-op для Stripe
}Правила: карты никогда не касаются наших серверов (виджет/редирект
провайдера; PCI DSS SAQ-A); у нас — payment_methods.token
провайдера + маска **** 1234 + бренд + срок. Выбор
провайдера — по региону покупателя и доступности (feature flag +
health).
9.3 Счета для юрлиц (безнал, акты, ЭДО — P1)
- Покупатель-юрлицо (РФ): реквизиты (ИНН/КПП/ОГРН, адрес, e-mail
бухгалтерии) → счёт на оплату (PDF, номер
MO-2027-000123) → оплата по реквизитам → сверка поступлений (выписка банка: импорт 1C-формата/API банка — P1; до того — ручная отметка в консоли владельца) → выпуск прав → акт/УПД (PDF) → ЭДО (Диадок/СБИС API — P1). Цифровой товар: НДС 20 % (льгота пп. 26 п. 2 ст. 149 НК РФ для программ в реестре отечественного ПО — применяется, если владелец включит продукт в реестр; тогда «без НДС», решение — раздел 16). - Юрлица ЕС/мира: инвойс Stripe с reverse charge по VAT ID (VIES-проверка), оплата картой/SEPA/wire.
- Срок оплаты счёта 14 дней; подписка организации выставляется за 30
дней до продления; неоплата →
past_dueпо тому же графику, что и карта (9.5), с письмами бухгалтерии и администратору.
9.4 Налоги
| Регион покупателя | Логика |
|---|---|
| РФ | цена включает НДС 20 % (или без НДС при льготе реестра); чек 54-ФЗ с
vat_code |
| ЕАЭС (BY, KZ, AM, KG) | цена в ₽ без российского НДС для юрлиц (место реализации — покупатель), для физлиц — НДС страны (P1: регистрация «налог на Google» в KZ/BY) |
| ЕС | Stripe Tax: VAT страны покупателя (OSS), B2B reverse charge |
| Остальные | Stripe Tax где обязаны (US sales tax по штатам порог экономического присутствия, UK, AU, …) |
9.5 Продления и дэнинг
| Момент | Действие |
|---|---|
| T−30 (год) / T−7 (месяц) | письмо «предстоящее списание», сумма, способ, ссылка на управление |
| T | списание по сохранённому способу; успех → invoice.paid
→ продление права (expires_at += период), чек |
| T (неуспех) | past_due; письмо 1 с ссылкой оплатить; повтор списания
T+3 |
| T+3, T+7 | повторы; письма 2, 3 (в приложении — баннер через
/validate: past_due) |
| T+14 | suspended; право истекает; письмо 4; в приложении — 7
дней мягкого режима, затем ReadOnly |
| T+14 … T+90 | восстановление оплатой в один клик (та же подписка), облако только чтение |
| T+90 | expired; облако удалено; восстановление = новая
покупка |
| Отмена пользователем | canceled_at_period_end; доступ до конца оплаченного
периода; письмо-подтверждение; возврат — по 9.6 |
Смена способа оплаты — через портал (виджет провайдера, новый токен). Смена цены — для действующих подписчиков новая цена применяется со следующего периода с уведомлением за 30 дней (письмо + баннер).
9.6 Возвраты
- Самообслуживание в портале в течение 14 дней после покупки (≤ 2
активаций, трафик облака < 1 ГБ) — автоматически; иначе — заявка в
поддержку (ответ 2 раб. дня). Возврат →
refundу провайдера → чек возврата (54-ФЗ) → права отзываются (reason: refund), лицензии в CRL. Частичный возврат Business — пропорционально неиспользованным местам и времени (решение владельца: только по закону/договору). - Chargeback/dispute: автоматический отзыв прав, аккаунт —
lockedдо разбирательства, доказательства провайдеру (журнал активаций, IP, согласие с офертой).
9.7 Вебхуки провайдеров → лицензии
sequenceDiagram
autonumber
participant Pv as Провайдер (ЮKassa/CloudPayments/Stripe)
participant B as billing-service
participant Q as Очередь событий (Redis Streams/NATS)
participant L as license-service
participant M as Почта
Pv->>B: POST /webhooks/{provider} (подпись / IP-allowlist)
B->>B: verify_webhook() · INSERT webhook_events (event_id UNIQUE) — дубликат → 200 без действий
B->>B: транзакция: payment succeeded → invoice paid → subscription active / order fulfilled
B->>Q: entitlement.upsert {customer, account_id|org_id, edition, kind, seats, valid_from, expires_at, source: invoice}
Q->>L: consumer (at-least-once, идемпотентно по invoice_id)
L->>L: UPSERT entitlement · stale для активных лицензий · при первом заказе без ключа — выпуск ключа (для показа в портале)
L-->>Q: entitlement.ready
B->>M: письмо: чек/инвойс, ключ (если без аккаунта), ссылка на скачивание
B-->>Pv: 200 OK (в пределах 10 с, тяжёлая работа — асинхронно)
Повторная доставка вебхука безопасна (UNIQUE event_id);
потерянный вебхук — сверка reconcile раз в час по API
провайдера по платежам в pending старше 15 мин.
9.8 Антифрод
- Velocity-правила: ≤ 3 попытки оплаты/час на аккаунт, ≤ 5 карт/сутки, ≤ 2 новых аккаунта/сутки с IP (мягко: капча); 3-D Secure обязателен (провайдер); несоответствие страны карты и региона цены → проверка/отказ (региональные цены).
- Правила trial-злоупотреблений: отпечаток + e-mail-домен одноразовых почт (список) → trial недоступен.
- Chargeback-rate мониторинг; автоматический отзыв прав по
charge.dispute.created; списки блокировок (e-mail хэш, отпечаток, BIN) с TTL; ручной разбор в консоли владельца. - Ключи: партии реселлеров с
activate_before, геоограничениеregion, лимит активаций/сутки на партию (всплеск → алерт).
9.9 Партнёры и реселлеры
- Кабинет реселлера (портал,
organizations.kind = reseller): заказ партий ключей (счёт → оплата → партияlive), остатки, статусы ключей (activated/not, без ПДн покупателя), отчёт продаж, возврат неактивированных ключей (180 дней), скидка по уровню (Registered −20 %, Silver −25 %, Gold −35 %; критерии — оборот за год). - Партнёрские (NFR,
kind=partner) ключи — бесплатные лицензии для демонстраций: 1–5 мест, 1 год, в CRL при разрыве партнёрства. - Реферальная программа (P2): промокод партнёра, вознаграждение 15 % первого платежа.
- Маркетплейсы (Softline, Merlion, Microsoft/Apple Store — не применимо для десктопа; P2) — через партии ключей.
9.10 Образовательная верификация
- Организация
kind=education: заявка с доменом учебного заведения (*.edu,*.ac.*, список российских вузов/школ по справочнику РОН/ЕГРЮЛ с ОКВЭД 85.x), документ (лицензия на образовательную деятельность) → ручная проверка ≤ 3 раб. дней →edu_verifications.status = approved, срок 12 месяцев, продление автоматическое при подтверждении домена (письмо на домен). - Учащиеся — через домен e-mail учебного заведения или приглашение администратора; личная Education-подписка (P2) — по студенческому (загрузка фото, ручная проверка) на 12 месяцев.
10. Серверные сервисы
10.1 Архитектура
Интернет / сеть организации
│ HTTPS (TLS 1.3), HTTP/2
┌──────▼──────┐
│ edge: Traefik/nginx + WAF, rate-limit L7, GeoIP → регион │
└──┬───────┬────────┬────────┬─────────┬────────┬──────────┘
│ │ │ │ │ │
id-service license- billing- portal update- cloud-service (ф. 6)
(axum) service service (web, service + telemetry-service
(axum) (axum) Leptos/ (static
axum) + axum)
│ │ │ │
┌─────────▼───────▼────────▼────────▼─────────┐ mTLS между сервисами (внутренний CA, SPIFFE-id,
│ PostgreSQL 16 (кластер региона, Patroni, PITR)│ сертификаты 24 ч, ротация cert-manager/step-ca)
│ Redis 7 (rate-limit, idempotency, lease-hot, │
│ jwks/CRL cache, очереди Streams) │
│ S3-совместимое (MinIO/облако): .mlic-архив, │
│ инвойсы PDF, экспорты, CRL │
│ HSM (YubiHSM 2 / КриптоПро HSM / CloudHSM): │
│ Ed25519 подпись лицензий и CRL, │
│ K_issuer HMAC, pepper, KMS-ключи │
└───────────────────────────────────────────────┘
- Один репозиторий
services/(cargo workspace): общие крейтыmo-common(конфиг, трейсинг, ошибки, идемпотентность),mo-auth(JWT/JWKS, mTLS-идентичность),mo-db(sqlx, миграции),mo-license-core(тот же код формата ключа/лицензии, что и в клиенте — одна реализация,kernel/crates/meridian-licenseзависит от него через featureno-net). - Между сервисами — HTTP/JSON по mTLS + события в Redis Streams
(
billing→license: entitlement.upsert,license→id: device.*,id→license: user.deleted, org.policy_updated); доставка at-least-once, потребители идемпотентны поevent_id. - Деплой: Docker-образы (distroless, arm64/amd64), Kubernetes в облаке
издателя; on-prem —
docker compose(10.9).
10.2 API id-service (основное)
| Метод и путь | Назначение | Auth |
|---|---|---|
POST /v1/auth/register |
регистрация (e-mail, пароль, согласия, регион) | — |
POST /v1/auth/login |
пароль → сессия (или mfa_required +
mfa_token) |
— |
POST /v1/auth/mfa/verify |
TOTP/код восстановления | mfa_token |
POST /v1/auth/magic-link,
GET /v1/auth/magic-link/{token} |
ссылка-вход | — |
GET /oauth/authorize, POST /oauth/token,
POST /oauth/revoke |
OAuth 2.1 для десктоп-клиента (PKCE, loopback) и refresh-ротация | — / device-proof |
GET /.well-known/openid-configuration,
/.well-known/jwks.json |
discovery, ключи JWT | — |
| `GET /v1/auth/providers/{google | apple | microsoft |
GET /v1/sso/{org_slug}/start,
POST /v1/sso/saml/acs,
GET /v1/sso/oidc/callback |
корпоративный SSO | — |
GET /v1/webauthn/register/options,
POST /v1/webauthn/register, …/login/options,
…/login |
passkeys | сессия / — |
GET/PATCH /v1/me, GET /v1/me/sessions,
DELETE /v1/me/sessions/{id},
GET/PATCH/DELETE /v1/me/devices/{id} |
профиль, сессии, устройства | Bearer |
POST /v1/me/export, POST /v1/me/delete,
POST /v1/me/delete/cancel |
экспорт/удаление | Bearer + reauth |
POST /v1/orgs, GET/PATCH /v1/orgs/{id},
GET/POST/PATCH/DELETE /v1/orgs/{id}/members[/{uid}],
…/groups, …/invites |
организации | Bearer (роль) |
GET/PUT /v1/orgs/{id}/policy,
GET /v1/orgs/{id}/policy/versions |
политики | admin |
GET/POST/PATCH/DELETE /v1/orgs/{id}/sso,
…/domains[/verify], …/api-keys |
SSO, домены, ключи API | admin |
GET /v1/orgs/{id}/audit?from&to&action&cursor |
аудит | auditor+ |
/scim/v2/* |
SCIM | api_key(scim) |
GET /internal/v1/users/{id},
GET /internal/v1/orgs/{id}/members/{uid}/permissions |
для других сервисов | mTLS |
10.3 API license-service
Полная спецификация с примерами —
spec/license/api-license-service.md. Сводка:
| Метод и путь | Назначение | Auth | Лимит |
|---|---|---|---|
POST /v1/activate |
ключ + устройство → лицензия | — / Bearer (если ключ требует аккаунт) | 10/мин IP, 30/сут отпечаток |
POST /v1/validate |
периодическая проверка, перевыпуск, статус подписки | подпись устройства (device_pubkey из лицензии) |
12/сут лицензия |
POST /v1/deactivate |
освободить место | подпись устройства / Bearer | 10/сут |
GET /v1/entitlements,
POST /v1/entitlements/{id}/activate |
права аккаунта/организации, активация входом | Bearer | 60/мин |
POST /v1/trial/start |
серверный пробный период | — | 5/сут IP |
POST /v1/offline/request |
.mreq → .mlic (портал, консоль,
on-prem) |
Bearer / api_key | 100/сут |
POST /v1/offline/deactivate |
.mreq kind=deactivate |
Bearer | 30/сут |
POST /v1/float/lease, /renew,
/release, /checkout,
GET /v1/float/status,
POST /v1/float/leases/{id}/revoke |
плавающие лицензии (on-prem) | подпись устройства / admin | — |
POST /v1/transfer |
перенос лицензии на новое устройство одним вызовом (деактивация+активация) | Bearer | 3/30 дн. право |
GET /v1/crl, GET /v1/signing-keys,
GET /v1/time |
CRL (ETag), публичные ключи, подписанное время | — | 60/мин |
GET /v1/catalog?region= (прокси в billing) |
цены для Hub | — | 60/мин |
GET/POST /v1/orgs/{id}/seats,
…/seats/{uid}, …/devices, …/keys,
…/offline/batch, …/float/servers |
консоль администратора | Bearer (license_admin) | 600/мин |
POST /internal/v1/keys/batches,
…/keys/revoke, …/licenses/{id}/revoke,
…/entitlements |
keygen, поддержка, billing | mTLS / HSM-подпись запроса (keygen) | — |
Форматы (сокращённо):
POST /v1/activate HTTP/1.1
Host: license.meridian-office.ru
Content-Type: application/json
Idempotency-Key: 6f1c3b2e-2a7b-4e1a-9d3c-5b2f1a9e8c7d
X-Meridian-Client: hub/1.2.0 (windows 11; x86_64; build 1.2.0.418)
{ "key": "MRDN-28800-C0G3N-EQW3V-00JBK-T2E7W",
"device": { "fingerprint_hash": "…", "components": ["…","…","…"], "name": "IVAN-DESKTOP",
"os": "windows", "os_version": "11.0.26100", "arch": "x86_64", "virtual": false, "device_pubkey": "base64:…" },
"client": { "app": "hub", "version": "1.2.0", "build": "1.2.0.418", "locale": "ru-RU" } }
{ "license_id": "lic_01J9ZK4M2XQ8R7S6T5V4W3Y2Z1", "license": "base64:<.mlic bytes>", "license_json": { "…": "развёрнутый payload для отладки/UI" },
"seats_used": 1, "seats_total": 3, "reused": false, "server_time": "2027-03-16T09:12:44Z", "crl_seq": 1042,
"next_validation_at": "2027-04-15T09:12:44Z" }10.4 Коды ошибок (общие для сервисов)
| Код | HTTP | Повтор | Описание |
|---|---|---|---|
VALIDATION_ERROR |
400 | нет | тело не соответствует схеме (details.fields) |
KEY_INVALID_FORMAT, KEY_CHECKSUM |
400 | нет | формат/контрольный символ |
UNAUTHENTICATED |
401 | после входа | нет/просрочен токен, неверная подпись устройства |
FORBIDDEN, ORG_POLICY_DENIED,
KEY_REGION_MISMATCH |
403 | нет | нет прав / политика / регион |
KEY_NOT_FOUND, ENTITLEMENT_NOT_FOUND,
LICENSE_NOT_FOUND, FLOAT_LEASE_NOT_FOUND |
404 | нет | |
ACCOUNT_REQUIRED, KEY_ALREADY_BOUND,
KEY_EXHAUSTED, DEVICE_LIMIT,
FLOAT_POOL_EXHAUSTED, IDEMPOTENCY_CONFLICT,
TRIAL_ALREADY_USED, REQUEST_REPLAYED |
409 | по ситуации | конфликт состояния |
KEY_REVOKED, LICENSE_REVOKED,
KEY_EXPIRED, ENTITLEMENT_EXPIRED,
REQUEST_EXPIRED |
410 | нет | |
SUBSCRIPTION_PAST_DUE |
402 | после оплаты | details.pay_url |
CLIENT_TOO_OLD |
426 | после обновления | details.min_client_version |
TRANSFER_LIMIT, RATE_LIMITED |
429 | retry_after |
|
SIGNING_UNAVAILABLE |
503 | да | HSM недоступен — клиент использует льготный период |
INTERNAL |
500 | да (экспоненциально) |
Идемпотентность: все POST принимают Idempotency-Key
(UUID), ответ кэшируется в Redis 24 ч по (ключ, субъект, хэш тела);
другое тело с тем же ключом → IDEMPOTENCY_CONFLICT. Лимиты
частоты — token bucket в Redis (rate:{route}:{subject}),
заголовки RateLimit-Limit/Remaining/Reset,
Retry-After.
10.5 Безопасность сервисов
- TLS 1.3 снаружи; внутри — mTLS (внутренний CA
step-ca; идентичности
spiffe://meridian/ns/prod/sa/license-service), авторизация вызовов по идентичности (billing может вызывать/internal/v1/entitlements, portal — нет). - HSM: ключи подписи лицензий/CRL (Ed25519) и
K_issuerне покидают HSM; license-service — PKCS#11 (cryptoki), пул сессий, подпись ≈ 2–5 мс; при недоступности HSM —SIGNING_UNAVAILABLE(валидация без перевыпуска продолжает работать: ответstatus: okподписывается отдельным «ответным» ключом с коротким сроком, кэшированным в памяти — P1). On-prem: ключ сервера — файл с паролем или PKCS#11 (поддержка Рутокен/КриптоПро HSM — Enterprise РФ). - Секреты — Vault/KMS, в контейнеры через файлы, не env; ротация секретов провайдеров платежей — по регламенту.
- Запросы keygen к
/internal/v1/keys/batchesподписываются ключом keygen (HSM) — сервер не создаёт партии сам.
10.6 Наблюдаемость, SLA, резервное копирование
| Аспект | Решение |
|---|---|
| Трассировка/метрики/логи | OpenTelemetry → Tempo / Prometheus / Loki; Grafana;
структурированные логи JSON без ПДн (e-mail хэшируется, ключи
маскируются MRDN-28800-…-T2E7W) |
| Ключевые метрики | activate_total{result},
validate_total{status}, license_sign_seconds,
hsm_errors, float_pool_in_use,
webhook_lag_seconds, rate_limited_total,
trial_start_total, entitlement_upsert_lag |
| Алерты | ошибки активаций > 5 % за 5 мин, HSM недоступен, лаг вебхуков > 5 мин, CRL не обновлялся при наличии отзывов, всплеск активаций партии |
| SLA | id/license: 99.9 % в месяц (≤ 43 мин простоя); клиент переживает простой за счёт офлайн-лицензий; billing/portal: 99.5 %; Enterprise on-prem — SLA поддержки по договору |
| Резервное копирование | PostgreSQL: WAL-архив + базовые бэкапы (pgBackRest) → PITR 30 дней, ежедневный снапшот 1 год, межрегиональная копия шифрованная; Redis — не критичен (восстанавливается); S3 — версионирование; HSM — резервные копии ключей в сейфе (wrapped); учение по восстановлению — ежемесячно, RTO 1 ч, RPO 5 мин |
| Нагрузка | расчётно: 1 млн активных устройств × /validate раз в 7
дней ≈ 1.7 rps средних, пики ×50 при релизах; активации ≈ 10 rps пик;
HSM 200 подписей/с достаточно с запасом |
10.7 Совместимость клиент–сервер
- API версионируется путём (
/v1); несовместимые изменения —/v2с параллельной поддержкой ≥ 24 месяца. - Сервер поддерживает клиентов двух мажорных версий
продукта;
min_client_versionв ответах/validateи в CRL-манифесте; клиент ниже минимума получаетCLIENT_TOO_OLDтолько для операций изменения (активация), проверка продолжает работать. .mlic format_version: клиент принимает≤своей; сервер выпускает формат не выше, чем указано вclientзапроса (supported_license_formats: [1]), поэтому старый клиент никогда не получит нечитаемую лицензию.- Неизвестные поля в JSON/CBOR игнорируются; новые флаги
featuresнеизвестны старому клиенту — просто не применяются.
10.8 Портал
(account.meridian-office.<tld>)
Личный кабинет: покупки, подписки (управление, отмена, способ
оплаты), ключи (показать, привязать, отвязать), устройства,
офлайн-активация, счета/чеки, профиль/безопасность (2FA, сессии,
passkeys), экспорт/удаление. Консоль организации (11.6). Кабинет
реселлера. Консоль издателя (внутренняя, отдельный хост
ops.…, только VPN + passkey): партии, отзыв, поиск по
ключу/лицензии, возвраты, верификации, метрики (раздел 14). Технология —
Rust (axum + Leptos SSR) или SvelteKit; решение — на фазе 1 с учётом
команды.
10.9 On-prem комплект для Enterprise
# deploy/onprem/docker-compose.yml (фрагмент)
services:
license-server:
image: registry.meridian-office.ru/onprem/license-service:1.4.2 # или tarball для air-gap
environment: [MO_MODE=onprem, MO_SERVER_LICENSE=/etc/mo/server.mlic, MO_SIGNING_KEY=/etc/mo/server.key, MO_DB_URL=…]
volumes: ["./server.mlic:/etc/mo/server.mlic:ro", "./server.key:/etc/mo/server.key:ro"]
ports: ["8443:8443"]
id-service: { image: …/onprem/id-service:1.4.2, profiles: ["sso"] } # опционально: локальный Meridian ID + SSO
cloud-service: { image: …/onprem/cloud-service:1.4.2, profiles: ["cloud"] } # фаза 6
update-mirror: { image: …/onprem/update-mirror:1.4.2 } # зеркало обновлений для сети без интернета
postgres: { image: postgres:16 }
redis: { image: redis:7 }
admin-console: { image: …/onprem/portal:1.4.2 } # веб-консоль администратора (локальная)- Лицензия самого сервера
server.mlic: выпускается издателем наorg_id, содержитdelegation{delegate_pubkey, max_pool, may_issue},expires_at(срок договора + 90 дней льготы), пул по типам (float: 200,volume: 500). Сервер при старте проверяет её; по истечении — продолжает выдавать аренды 90 дней с предупреждением, затем — только продление существующих (клиенты не теряют работу мгновенно). - Обновления сервера: образы из registry издателя (с доступом по
токену организации) или подписанные tarball для air-gap;
update-mirrorраздаёт клиентские обновления внутри сети, политикой задаётся канал и отложенный выпуск. - Отчётность издателю: не требуется (air-gap); по договору —
ежеквартальный экспорт
usage.json(подписанный) вручную. - Совместимость: сервер on-prem поддерживает клиентов на 2 мажорные версии; клиент показывает предупреждение, если сервер старше.
11. Клиентская часть
11.1
Крейт meridian-license
(kernel/crates/meridian-license)
Без сети по умолчанию (feature net включает HTTP-клиент
reqwest для /activate, /validate;
Hub-брокер — единственный процесс с net, документ-процессы
читают состояние из общей памяти/файла и получают события по IPC).
pub struct LicenseManager { /* … */ }
impl LicenseManager {
/// Глобальный экземпляр процесса / process-wide instance (загружает лицензии при первом обращении)
pub fn current() -> &'static LicenseManager;
pub fn state(&self) -> LicenseState; // снимок, дёшево (ArcSwap)
pub fn features(&self) -> Features; // объединение всех действующих лицензий + ядро
pub fn has(&self, f: Feature) -> bool { self.features().has(f) }
pub fn access(&self, app: AppId) -> AppAccess; // Full | ReadOnly | Hidden
pub fn policy(&self) -> Policy; // effective: сервер ∧ локальные источники
pub fn limits(&self) -> Limits;
pub fn subscribe(&self, cb: impl Fn(&LicenseEvent) + Send + Sync + 'static) -> Subscription;
pub fn reload(&self) -> Result<()>; // перечитать файлы (после обновления Hub-брокером)
// --- только с feature "net" (Hub-брокер) ---
pub async fn activate_key(&self, key: &str, account: Option<&AccessToken>) -> Result<ActivationOutcome, LicenseError>;
pub async fn activate_entitlement(&self, id: &str, token: &AccessToken) -> Result<ActivationOutcome, LicenseError>;
pub async fn start_trial(&self) -> Result<ActivationOutcome, LicenseError>;
pub async fn validate_now(&self) -> Result<ValidationOutcome, LicenseError>;
pub async fn deactivate(&self, license_id: &str) -> Result<(), LicenseError>;
pub fn export_request(&self, kind: RequestKind, key: Option<&str>) -> Result<OfflineRequest>; // .mreq
pub fn import_license(&self, bytes: &[u8]) -> Result<LicenseState, LicenseError>; // .mlic
pub async fn float_lease(&self, server: &Url, pool: &str) -> Result<Lease, LicenseError>;
}
#[derive(Clone, Debug)]
pub enum LicenseStatus {
NotActivated,
Trial { days_left: u16 },
Active { edition: Edition, kind: Kind, expires_at: Option<DateTime>, seats_used: u16, seats_total: u16 },
Expiring { days_left: u16 }, // ≤ 14 дней до expires_at
Grace { days_left: u16, reason: GraceReason }, // PastDue | OfflineGrace | Soft
Expired { read_only: bool }, // read_only всегда true; документы открываются
Revoked { reason: RevokeReason },
DeviceMismatch, RebindPending,
DeviceLimitExceeded { devices: Vec<DeviceInfo> },
Leased { expires_at: DateTime, offline_reserve_until: DateTime, server: Url },
WaitingForPool { position: u32 },
}
pub enum LicenseEvent { Changed(LicenseState), FeaturesChanged(Features), ValidationFailed(LicenseError), ClockAnomaly }- Кэш: состояние в
ArcSwap<LicenseState>; подпись проверяется при загрузке и раз в час (фоновый таймер в Hub); в документ-процессах — при старте и по событию IPClicense.changed(Hub пишет файл атомарно и шлёт сигнал). - Гейтинг в коде — только
LicenseManager::current().has(Feature::X); clippy-lint в репозитории (disallowed-methods) запрещает читатьeditionнапрямую вне крейта. - Проверки целостности: хэш публичных ключей зашит в двух местах
(константа + вычисление из таблицы), результат
verifyдублируется в ядре при сохранении в облако и запуске скриптов (раздел 12).
11.2 Крейт
meridian-id (kernel/crates/meridian-id)
pub struct IdClient { /* issuer, client_id = "meridian-desktop", store: Box<dyn TokenStore> */ }
impl IdClient {
/// OAuth 2.1 Authorization Code + PKCE (S256), loopback-редирект, системный браузер
pub async fn login(&self, opts: LoginOptions) -> Result<Session, IdError>; // opts: org_hint, prompt=login, sso_domain
pub async fn access_token(&self) -> Result<AccessToken, IdError>; // из кэша; обновляет за 60 с до истечения
pub async fn refresh(&self) -> Result<AccessToken, IdError>; // ротация refresh + device-proof
pub async fn logout(&self, everywhere: bool) -> Result<(), IdError>;
pub fn profile(&self) -> Option<Profile>; // кэш профиля (имя, e-mail, орг, роли)
pub fn subscribe(&self, cb: impl Fn(&IdEvent)) -> Subscription; // LoggedIn/LoggedOut/TokenRefreshed/Reauth
}
/// Хранилище токенов / token store: macOS Keychain (SecItem), Windows Credential Manager + DPAPI (CredWrite, CRED_PERSIST_LOCAL_MACHINE),
/// Linux Secret Service (libsecret; fallback — файл с ключом из kernel keyring при отсутствии демона, с предупреждением)
pub trait TokenStore { fn get(&self, k: &str) -> Result<Option<Secret>>; fn put(&self, k: &str, v: Secret) -> Result<()>; fn delete(&self, k: &str) -> Result<()>; }Поток loopback: клиент слушает
http://127.0.0.1:<случайный порт>/callback;
state и code_verifier одноразовые; после
получения code — обмен на токены, страница «Можно вернуться
в приложение», автозакрытие. Если браузер недоступен (сервер без GUI,
киоск) — device-flow (/oauth/device, код на экране, P1).
Токены — только в TokenStore; записи:
id.refresh, id.access (кэш),
id.device_key, license.clock_anchor,
license.trial_local,
license.key.<fingerprint>.
11.3 Интеграция в Hub и приложения
- Hub — брокер: единственный процесс с сетью для
лицензий/аккаунта; значок аккаунта в заголовке (👤 → меню: имя,
редакция, «Аккаунт и лицензия», «Выйти»); карточка состояния в стартовом
окне (trial, истечение, нет сети); проверка обновлений приносит CRL;
очередь
/validate. - Приложения: при старте читают состояние; гейтинг —
команда с замком в ribbon/меню/палитре
(
Command.requires = Feature::X): команда видна, по нажатию — всплывающее окно AC-08 «Доступно в Pro» с кратким объяснением и кнопкой «Узнать больше / Перейти»; документ никогда не блокируется; режимReadOnlyприложения — баннер сверху холста с причиной и кнопкой. - Параметры → Аккаунт и лицензия (AC-06): состояние, редакция, устройства (N из M), срок, канал обновлений, кнопки «Ввести ключ», «Войти/Выйти», «Деактивировать это устройство», «Активировать офлайн», «Импортировать лицензию», «Обновить состояние», «Управлять в портале»; для организаций — название, администратор, политики (только чтение).
11.4 Состояния UI и тексты сообщений
| Состояние | Где показывается | Заголовок | Текст | Действия |
|---|---|---|---|---|
| Не активировано (Free) | Параметры; значок без бейджа | «Meridian Office Free» | «Доступны Write, Sheets, Slides, PDF-просмотр и Math. Остальные приложения открывают файлы для чтения и печати.» | Ввести ключ · Войти · Попробовать Pro |
| Пробный период N дн. | бейдж Hub; баннер при N ≤ 7 | «Pro · пробный период» | «Осталось N дн. Затем приложения Pro перейдут в режим чтения и печати; файлы останутся доступны.» | Купить Pro · Ввести ключ |
| Активно | Параметры; значок с галочкой | «Pro · активно» | «Подписка до 1 февраля 2028. Устройство 3 из 5.» | Управлять · Деактивировать |
| Истекает (≤ 14 дн.) | баннер Hub (один раз в день), Параметры | «Подписка истекает через N дн.» | «Продление спишется 1 февраля. Проверьте способ оплаты.» / «Автопродление выключено.» | Продлить · Способ оплаты |
| Не оплачено (past_due) | жёлтый баннер во всех окнах (сворачиваемый) | «Не удалось продлить подписку» | «Повторим попытку 4 марта. Через N дн. приложения Pro перейдут в режим чтения и печати.» | Оплатить · Изменить способ |
| Истекло — только чтение и печать | баннер над холстом Pro-приложения | «Режим чтения и печати» | «Подписка Pro истекла. Файл можно просматривать, печатать и экспортировать. Для редактирования продлите подписку.» | Продлить · Открыть в Free-приложении (если применимо) |
| Офлайн, льготный период | значок в строке состояния Hub; баннер при ≤ 7 дн. | «Нет связи с сервером лицензий» | «Последняя проверка 12 дн. назад. Подключитесь к интернету в течение N дн., иначе приложения Pro перейдут в режим чтения.» | Проверить сейчас · Активировать офлайн |
| Отозвано | диалог один раз + Параметры | «Лицензия отозвана» | «Причина: ключ отозван продавцом (партия 0010). Если вы купили ключ у официального партнёра, обратитесь в поддержку — мы перевыпустим лицензию.» | Поддержка · Ввести другой ключ |
| Превышен лимит устройств | диалог при активации | «Все устройства заняты» | «К этой лицензии привязано 5 из 5 устройств. Освободите одно, чтобы продолжить.» + список (имя, ОС, последняя активность) | Деактивировать выбранное · Управлять в портале |
| Устройство изменилось (2 из 3) | тихо; Параметры | «Устройство обновлено» | «Лицензия будет обновлена при следующей проверке.» | Проверить сейчас |
| Устройство не совпадает | диалог | «Лицензия выдана другому устройству» | «Файл лицензии относится к другому компьютеру. Активируйте это устройство ключом или входом.» | Ввести ключ · Войти |
| Плавающая: аренда | значок Hub | «Pro · плавающая лицензия» | «Сервер лицензий: license.corp.local. Продление автоматически. Офлайн-резерв до 14:00 завтра.» | Взять с собой на N дн. · Освободить |
| Плавающая: пул исчерпан | диалог/баннер | «Все лицензии заняты» | «Используются 200 из 200. Вы 3-й в очереди. Пока доступны функции Free.» | Ждать · Уведомить администратора |
| Часы переведены | предупреждение в Параметрах | «Системное время отстаёт» | «Часы компьютера отстают от времени последней проверки. Проверьте настройки времени.» | Открыть настройки времени |
Голос — по MDS: кратко, во втором лице, без восклицаний; в баннерах — не чаще одного в день для одного состояния.
11.5 Экраны для
макетов (mockups/account.html)
| ID | Экран | Содержание |
|---|---|---|
| AC-01 | Вход в Meridian ID | e-mail + пароль, «Войти по ссылке», кнопки Google/Apple/Microsoft/Яндекс/VK, «Корпоративный вход (SSO)», ссылка на регистрацию; в приложении — открывает системный браузер, в окне — состояние ожидания с кнопкой «Отмена» |
| AC-02 | Регистрация | e-mail, пароль с индикатором, согласия (152-ФЗ/политика/оферта — отдельные чекбоксы), регион (автоопределение), подтверждение e-mail |
| AC-03 | Второй фактор | ввод TOTP / passkey (кнопка «Использовать ключ безопасности») / код восстановления; «Запомнить это устройство 30 дней» |
| AC-04 | Активация ключом | поле MRDN- с автоформатированием групп, подсветка
контрольной ошибки, имя устройства (редактируемое), «Активировать», «Нет
сети — активировать офлайн», результат «устройство N из M» |
| AC-05 | Офлайн-активация | шаг 1: сохранить .mreq / показать QR / скопировать
текст; шаг 2: ссылка портала; шаг 3: импорт .mlic (кнопка
«Выбрать файл» и поле вставки текста); для администраторов — пакетный
режим |
| AC-06 | Менеджер лицензий (Параметры → Аккаунт и лицензия) | карточка аккаунта, карточка лицензии (редакция, срок, устройства), таблица лицензий (личная/организации/trial), устройства с кнопками, политики организации (read-only), журнал последних проверок |
| AC-07 | Баннер пробного периода | полоса в Hub: «Pro · пробный период · осталось 6 дн.» + «Купить» + «×» |
| AC-08 | Предложение апгрейда (замок) | всплывающее окно у команды: значок замка, «Сравнение документов доступно в Pro», 3 строки преимуществ, цена из каталога, «Перейти на Pro» (портал) · «Ввести ключ» · «Не сейчас» |
| AC-09 | Консоль администратора организации (веб) | разделы: Обзор (места, устройства, истечение), Участники и группы (SCIM/SSO-статус), Места (назначить/снять, импорт CSV), Устройства (освободить неактивные), Ключи и офлайн-пакеты, Плавающие серверы, Политики (форма + JSON), Аудит, Биллинг |
| AC-10 | Портал покупок | сравнение редакций (матрица 2.3 в коротком виде), выбор
периода/мест, промокод, региональная цена, оплата (виджет провайдера),
страница успеха с ключом/кнопкой «Открыть приложение»
(meridian://activate?entitlement=…) |
| AC-11 | Состояние плавающей лицензии | панель в Hub: сервер, аренда до, резерв, кнопка «Взять с собой», очередь; у администратора — график занятости пула |
| AC-12 | Устройства | список устройств аккаунта: имя, ОС, редакция, последняя активность, «Переименовать», «Деактивировать», «Удалить» |
| AC-13 | Режим чтения и печати | документ открыт, баннер причины над холстом, команды правки с замком, «Экспорт в PDF» активен |
Deep link
meridian://activate?entitlement=<id>&nonce=<n>
регистрируется установщиком; Hub принимает его, запрашивает
подтверждение и выполняет activate_entitlement (с входом
при необходимости).
12. Защита и анти-пиратство
12.1 Что делаем
| Мера | Реализация |
|---|---|
| Подпись лицензий | Ed25519, ключ в HSM; клиент проверяет при каждом запуске процесса и раз в час; два зашитых публичных ключа |
| Многократные проверки | verify() вызывается из трёх независимых мест ядра:
загрузчик лицензии, точка сохранения в облако
(meridian-sync), запуск скриптов
(meridian-script); каждое место хранит собственную копию
хэша публичного ключа — патч одного места не снимает остальные |
| Целостность сборки | подпись бинарей (EV на Windows, нотаризация macOS, подпись
Flatpak/deb/rpm); при старте Hub проверяет хэш-манифест установленных
файлов (integrity.json, подписан ключом сборки);
несовпадение → телеметрия (по согласию) и предупреждение, но не
блокировка |
| Обфускация ключевых проверок | только для 3 функций (verify, сравнение отпечатка,
Features::has): control-flow flattening + inline, сборка
через cargo-obfuscate/LLVM-пасс; константы (ключи, домены)
— распределены в нескольких таблицах и собираются на лету; имена
символов — strip в релизе. Умеренно: сборка должна оставаться
отлаживаемой |
| Серверная валидация подписок | подписка без сервера живёт ≤ 37 дней; перевыпуск лицензии — только сервером; нечего «патчить» надолго |
| Привязка к устройству | 2 из 3 компонентов; клоны ВМ видны серверу по
instance_id в /validate |
| Отзыв | CRL через update-service (один контакт с сетью в день приносит и обновления, и CRL) |
| Телеметрия аномалий | только по согласию и в агрегате: число лицензий с неверной подписью,
случаи несовпадения integrity.json, повторяющиеся
license_id на разных отпечатках; без идентификаторов
пользователей |
| Защита ключей на сервере | хранение ключей в зашифрованном виде, K_issuer в HSM,
партии с activate_before, лимиты активаций на партию |
12.2 Чего не делаем
- Никакого онлайн-DRM для вечных лицензий: вечная Home/Pro работает без сети бессрочно.
- Никакой блокировки документов, «водяных знаков» в экспорте Free, сокращённых функций сохранения.
- Никаких драйверов, служб уровня ядра, антиотладочных техник, которые ломают отладку честных пользователей и Linux-дистрибутивы.
- Никакой телеметрии без согласия; никаких скрытых «звонков домой» — список сетевых адресов приложения публикуется в документации.
- Никаких задержек/замедлений при подозрении на пиратство: либо лицензия валидна, либо редакция Free.
12.3 Реагирование на утечки
| Сценарий | Реакция |
|---|---|
| Ключ опубликован в сети (вечная Home) | лимит мест исчерпывается первыми активировавшими; при > 3
активаций/сутки на ключ — автоматическая пометка suspect,
отзыв с перевыпуском для первого активатора (по дате) |
| Утекла партия реселлера | отзыв партии (reason: leak), перевыпуск честным (7.8),
счёт реселлеру по договору |
| Генератор ключей | невозможен без K_issuer + записи в БД; фальшивые ключи
отбрасываются сервером; «кейген» для офлайн-режима невозможен — лицензию
подписывает только HSM |
| Патч клиента | следующее обновление меняет раскладку констант и обфускацию; подписка всё равно требует сервера; вечные — принимаем потери |
| Утечка приватного ключа подписи | ротация на reserve (5.6), CRL с
revoked_signing_keys, обязательное обновление клиента для
новых активаций |
| Утечка БД | ключи зашифрованы (KMS), пароли — Argon2id + pepper в HSM, токены — хэши; уведомление пользователей и регулятора (152-ФЗ — 24/72 ч, GDPR — 72 ч) |
12.4 Юридические меры
EULA запрещает обход защиты, передачу ключей, коммерческое использование Home; DMCA/запросы на удаление раздач (сервис-партнёр); претензии реселлерам по договору; уголовно-правовые меры (ст. 146 УК РФ, ст. 272/273) — только в системных случаях (продажа поддельных ключей в промышленном масштабе), по решению владельца.
13. Соответствие требованиям
| Область | Что делаем |
|---|---|
| 152-ФЗ | оператор ПДн — юрлицо владельца (уведомление Роскомнадзора); ПДн
граждан РФ записываются и хранятся первично в регионе ru
(ЦОД в РФ); отдельные согласия (consents): обработка ПДн,
политика конфиденциальности, маркетинг, телеметрия; политика обработки
ПДн опубликована; трансграничная передача — только в
агрегированном/обезличенном виде; локализация касается
id/billing/license/cloud (документы пользователей в облаке РФ — тоже в
РФ) |
| GDPR | DPA с процессорами (Stripe, хостинг, почта), регистр обработки, DPO (при необходимости), права субъекта: доступ/экспорт (8.9), исправление, удаление, ограничение, переносимость; законное основание — договор (оферта) и согласие (маркетинг/телеметрия); cookie-баннер портала — только необходимые по умолчанию |
| PCI DSS | карты не касаются серверов (SAQ-A): виджеты/редиректы провайдеров, токены; TLS-only; журналы без PAN |
| 54-ФЗ | чеки через ЮKassa/CloudPayments при каждой оплате/возврате; e-mail покупателя в чеке; подписки — чек на каждое списание |
| Реестр отечественного ПО | подготовка заявки (Минцифры) — вопрос владельца (влияет на НДС и госзаказчиков) |
| Хранение журналов | аудит организаций 1/3 года; журналы доступа серверов 1 год (обезличенные IP после 90 дней); бухгалтерия 5 лет; журнал выпуска ключей — бессрочно |
| Документы | Политика конфиденциальности (кто, что, зачем,
сроки, права, регионы, контакты); Оферта (физлица:
предмет, цены, порядок оплаты/возврата, автопродление с явным согласием,
расторжение); EULA (лицензия на ПО: редакции, число
устройств, коммерческое использование, запреты, обновления,
гарантии/ограничение ответственности, применимое право); Договор
для юрлиц (рамочный + спецификации); DPA (ЕС);
SLA Enterprise; Политика обработки ПДн
(152-ФЗ) — все версионируются, версия фиксируется в
consents |
14. Метрики и аналитика лицензирования
| Метрика | Определение | Источник | Цель (ориентир) |
|---|---|---|---|
| Trial → paid | доля trial, купивших в течение 60 дней от начала | trials ⨝ orders |
≥ 8 % |
| Trial start rate | доля первых запусков, включивших trial | telemetry (согласие) / trial/start |
≥ 40 % |
| Activation success | activate 2xx / все попытки |
метрики license-service | ≥ 97 % |
| Отказы активации по причинам | распределение кодов ошибок | метрики | KEY_EXHAUSTED < 1 %, KEY_NOT_FOUND <
1.5 % |
| Churn (месячный/годовой) | доля подписок, не продлившихся, по когортам | subscriptions |
≤ 4 %/мес, ≤ 25 %/год |
| Dunning recovery | доля past_due, вернувшихся в active |
subscriptions |
≥ 60 % |
| MRR / ARR, ARPU, LTV | стандартно; по редакциям и регионам | billing | — |
| Seat utilization (Business) | назначенные/оплаченные места; активные за 30 дней/назначенные | license | ≥ 80 % |
| Offline grace usage | доля лицензий, бывших в OfflineGrace > 7 дней |
/validate интервалы |
мониторинг |
| Float pool peak / denied | пик занятости, число отказов (on-prem — у клиента) | float_leases |
denied < 1 % |
| Revocations | число отзывов по причинам | revocations |
— |
| Device transfers | переносов на право в месяц; доля авто | activations |
— |
| Refund rate | возвраты / покупки | billing | ≤ 2 % |
| Support load | обращений по лицензированию на 1000 активаций | helpdesk | ≤ 5 |
Отчёты для владельца: ежедневный дайджест (письмо): продажи, активации, ошибки, алерты; еженедельный — воронка trial→paid, churn, топ ошибок, партии реселлеров; ежемесячный — финансовый (MRR/ARR/возвраты/налоги) и аудит безопасности (отзывы, подозрительные активации). Дашборды — Grafana (операционные) + консоль издателя (бизнес), выгрузка CSV. Все метрики — без ПДн, когорты ≥ 20 человек.
15. План
внедрения по фазам (связь с 08-roadmap.md)
| Фаза (библия §7) | Срок | Лицензирование и аккаунты — результат |
|---|---|---|
| 0 — Основание (0–3 мес) | Этот документ и spec/license/*; крейты
meridian-license (формат ключа, .mlic,
отпечаток, Features, LicenseManager без сети)
и meridian-id (TokenStore, PKCE-поток); каркас
services/{id,license,billing}-service (axum, миграции
db-schema.sql, /v1/time, /v1/crl,
/v1/activate с тестовым подписантом);
tools/keygen (CLI, файл-секрет для dev, YubiHSM — к фазе
1); ADMX/plist-шаблоны (черновик); CI с тестами формата ключа и лицензии
(векторы из license-key-format.md) |
|
| 1 — Ядро и Write (3–12 мес) | Trial (серверный + локальный); онлайн-активация ключом; вход в
Meridian ID (пароль, magic link, Google/Apple/Microsoft/Яндекс/VK, TOTP,
passkeys); портал (покупки, ключи, устройства, подписки, счета);
billing-service с ЮKassa + Stripe (CloudPayments — резерв), 54-ФЗ,
дэнинг, возвраты; продажа Home (вечная/подписка) и Pro (подписка/вечная
без облака); деактивация/перенос; CRL через update-service; экраны
AC-01…AC-08, AC-10, AC-12, AC-13; HSM в проде; регионы
ru/eu |
|
| 2 — Sheets (6–16 мес) | гейтинг Sheets-функций; реселлерские партии и кабинет реселлера; счета для юрлиц (ручная сверка); партнёрские NFR-ключи | |
| 3 — Slides и Draw (12–22 мес) | Business: организации, роли, консоль администратора (AC-09), назначение мест, SSO (OIDC/SAML), SCIM, политики, аудит; Education-верификация; ЭДО/акты (P1) | |
| 4 — Paint, PDF, Math (16–26 мес) | гейтинг новых приложений;
doc_sign/pdf_sign; региональные цены PPP;
device-flow входа |
|
| 5 — Notes, Mail, Base, Forms (22–34 мес) | гейтинг; forms_publish/forms_org_responses
через cloud; лимиты Assist в лицензии |
|
| 6 — Совместная работа и облако (28–40 мес) | Enterprise: on-prem комплект (license-server, id-service, cloud,
update-mirror), плавающие лицензии (AC-11), объёмные ключи с MDM/GPO,
офлайн-пакеты для администраторов, лицензия сервера с делегированием,
SIEM-экспорт аудита, doc_sign_gost |
|
| 7 — Экосистема (36–48 мес) | магазин скриптов/шаблонов (scripts_store,
template_store, выплаты авторам — отдельный документ),
партнёрская/реферальная программа, маркетплейсы, Publisher/Plan в
редакциях, веб-просмотрщик (если будет решение) без влияния на
лицензии |
Критерии готовности фазы 1 (лицензирование): 100 % тестовых векторов проходят на трёх ОС; активация в 1 действие ≤ 3 с; переустановка ОС не расходует место (проверено на Windows/macOS/Linux); подписка переживает 30 дней без сети; возврат и отзыв доходят до клиента ≤ 24 ч; покупка Home без аккаунта — от оплаты до активированного приложения ≤ 2 мин.
16. Вопросы к владельцу
- Цены: подтвердить диапазоны раздела 3 или задать свои; разовая Pro без облака — оставить?; месячная Business — нужна?
- Регионы и юрлицо: страна юрлица-продавца (РФ; нужно
ли второе юрлицо вне РФ для Stripe — Stripe не работает с российскими
юрлицами); какие регионы хранения на старте (только
ruили сразуru+eu). - Провайдеры платежей: ЮKassa как основной для РФ, CloudPayments — резерв/второй? Stripe — только при наличии зарубежного юрлица; альтернатива — Paddle/LemonSqueezy как merchant of record.
- Реестр отечественного ПО и льгота по НДС — подавать ли заявку (влияет на цены и госзаказ).
- Коробочная розница: нужны ли карточки-ключи (типография, SKU, партнёры) в фазе 1–2 или только электронные ключи.
- Trial без аккаунта — подтверждено (библия); нужна ли дополнительно привязка trial к e-mail для маркетинга (снижает конверсию).
- Валюта ЕАЭС: цены в ₽ или локальные валюты (KZT, BYN).
- Education: гранты (бесплатные кампусы) — по каким критериям; личная студенческая подписка — нужна ли.
- Политика возвратов: 14 дней автоматически для всех или только где требует закон.
- Поддержка Enterprise РФ: ГОСТ-подпись и КриптоПро HSM в on-prem — включать в фазу 6 или по первому договору.
- Телеметрия аномалий — включать ли предложение «помочь улучшить» при первом запуске (opt-in) или только в Параметрах.
- Домены:
meridian-office.ru+.com? (библия:<tld>) — нужны дляissuer, писем и зашитых адресов в фазе 1.
Вопросы к библии
Отступлений от решений раздела 9 нет. Уточнения, которые предлагается внести в библию при следующей ревизии:
- §9.3: контрольная часть ключа конкретизирована как «28 бит MAC + 5 бит публичного контрольного символа» (в библии — «контрольная часть»); серийник = партия (16) + случайные (32). Это уточнение, не изменение.
- §9.3: «проверка подписки раз в 7 дней, 30 дней льготы» — дополнено
правилом «7 дней мягкого режима после ответа
expiredпри наличии сети», чтобы неоплата не ломала рабочий день; предлагается зафиксировать. - §9.1 Home: «до 3 устройств» — предлагается уточнить «на одного владельца; аккаунт не требуется, перенос через ключ»; Business — «устройств на пользователя: 5» (библия молчит).
- §9.1: редакция Education — «как Business с ограничениями»: предложен
конкретный набор (без
assist_cloudпо умолчанию, облако 100 ГБ/пользователь, безsupport_sla); требует подтверждения. - §9.4: добавить
update-serviceкак канал доставки CRL иops-консоль издателя в список сервисов/доменов. - §9.2: регионы хранения «РФ / ЕС» — предложено ограничиться двумя кластерами с глобальным минимальным directory для маршрутизации входа.