Meridian Office

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 Просмотр, поиск, печать, заполнение форм без сохранения pdf_view ✓ ✓ ✓ ✓ ✓ ✓
26 PDF Аннотации, редактирование текста/страниц, объединение pdf_edit — ✓ ✓ ✓ ✓ ✓
27 PDF Создание и сохранение форм pdf_forms — ✓ ✓ ✓ ✓ ✓
28 PDF Электронная подпись PDF (PAdES) (предл.) pdf_sign — — ✓ ✓ ✓ ✓
29 PDF Конвертация PDF → .docx/.mwx (через Write) (предл.) pdf_convert — — ✓ ✓ ✓ ✓
30 PDF OCR сканов (локальный движок) (предл.) pdf_ocr — — ✓ ✓ ✓ ✓
31 Math Редактор формул во всех приложениях core_math ✓ ✓ ✓ ✓ ✓ ✓
32 Notes Записные книжки, рукописный ввод, вырезки app_notes — ✓ ✓ ✓ ✓ ✓
33 Mail Почта, контакты, задачи app_mail — ✓ ✓ ✓ ✓ ✓
34 Mail Календарь (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)

Состояния пробного периода:

Состояние Условие Поведение 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 Как цены живут в системе


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)

Обоснование раскладки:

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


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": "…" }

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 Ключи подписи и ротация

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":{…} }

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 }
}

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 Виртуальные машины, терминальные серверы, контейнеры


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»

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:…" } }

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 дней по политике → место занято на весь срок)

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

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

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)

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 → все окна снимают замки без перезапуска

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

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": "…" }

8.4 SCIM 2.0

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

8.9 Удаление и экспорт данных

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)

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 Возвраты

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 Антифрод

9.9 Партнёры и реселлеры

9.10 Образовательная верификация


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-ключи      │
          └───────────────────────────────────────────────┘

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 Безопасность сервисов

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 Совместимость клиент–сервер

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 }   # веб-консоль администратора (локальная)

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 }

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 и приложения

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 Чего не делаем

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. Вопросы к владельцу

  1. Цены: подтвердить диапазоны раздела 3 или задать свои; разовая Pro без облака — оставить?; месячная Business — нужна?
  2. Регионы и юрлицо: страна юрлица-продавца (РФ; нужно ли второе юрлицо вне РФ для Stripe — Stripe не работает с российскими юрлицами); какие регионы хранения на старте (только ru или сразу ru + eu).
  3. Провайдеры платежей: ЮKassa как основной для РФ, CloudPayments — резерв/второй? Stripe — только при наличии зарубежного юрлица; альтернатива — Paddle/LemonSqueezy как merchant of record.
  4. Реестр отечественного ПО и льгота по НДС — подавать ли заявку (влияет на цены и госзаказ).
  5. Коробочная розница: нужны ли карточки-ключи (типография, SKU, партнёры) в фазе 1–2 или только электронные ключи.
  6. Trial без аккаунта — подтверждено (библия); нужна ли дополнительно привязка trial к e-mail для маркетинга (снижает конверсию).
  7. Валюта ЕАЭС: цены в ₽ или локальные валюты (KZT, BYN).
  8. Education: гранты (бесплатные кампусы) — по каким критериям; личная студенческая подписка — нужна ли.
  9. Политика возвратов: 14 дней автоматически для всех или только где требует закон.
  10. Поддержка Enterprise РФ: ГОСТ-подпись и КриптоПро HSM в on-prem — включать в фазу 6 или по первому договору.
  11. Телеметрия аномалий — включать ли предложение «помочь улучшить» при первом запуске (opt-in) или только в Параметрах.
  12. Домены: meridian-office.ru + .com? (библия: <tld>) — нужны для issuer, писем и зашитых адресов в фазе 1.

Вопросы к библии

Отступлений от решений раздела 9 нет. Уточнения, которые предлагается внести в библию при следующей ревизии:

  1. §9.3: контрольная часть ключа конкретизирована как «28 бит MAC + 5 бит публичного контрольного символа» (в библии — «контрольная часть»); серийник = партия (16) + случайные (32). Это уточнение, не изменение.
  2. §9.3: «проверка подписки раз в 7 дней, 30 дней льготы» — дополнено правилом «7 дней мягкого режима после ответа expired при наличии сети», чтобы неоплата не ломала рабочий день; предлагается зафиксировать.
  3. §9.1 Home: «до 3 устройств» — предлагается уточнить «на одного владельца; аккаунт не требуется, перенос через ключ»; Business — «устройств на пользователя: 5» (библия молчит).
  4. §9.1: редакция Education — «как Business с ограничениями»: предложен конкретный набор (без assist_cloud по умолчанию, облако 100 ГБ/пользователь, без support_sla); требует подтверждения.
  5. §9.4: добавить update-service как канал доставки CRL и ops-консоль издателя в список сервисов/доменов.
  6. §9.2: регионы хранения «РФ / ЕС» — предложено ограничиться двумя кластерами с глобальным минимальным directory для маршрутизации входа.