From 53b73147057746718950479e28710fe1b943da97 Mon Sep 17 00:00:00 2001 From: vvzvlad Date: Thu, 18 Jun 2026 22:54:23 +0300 Subject: [PATCH] docs(mcp): detail iOS AGPL licensing blocker in plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a dedicated section describing the licensing conflict between the AGPL‑3.0‑licensed web client and App Store DRM/usage rules. Explain why this is a non‑technical blocker, outline possible distribution approaches (server‑loaded client, OTA updates, PWA, sideload), and recommend confirming the chosen path before implementing any iOS wrapper code. --- docs/mobile-app-plan.md | 131 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 127 insertions(+), 4 deletions(-) diff --git a/docs/mobile-app-plan.md b/docs/mobile-app-plan.md index f4660454..65b5ea0f 100644 --- a/docs/mobile-app-plan.md +++ b/docs/mobile-app-plan.md @@ -34,6 +34,11 @@ 6. **Оффлайн-будущее уже заложено** (Yjs + `y-indexeddb`). Детальный план — в [offline-sync-plan.md](offline-sync-plan.md); мобильное приложение этот план переиспользует, а не дублирует. +7. **Главный блокер — не технический, а лицензионный.** AGPL форка несовместима + с условиями App Store, если зашивать веб-клиент в бинарник: DRM/usage-rules + Apple = «дополнительные ограничения», запрещённые AGPLv3 §10. Развязки — + грузить клиент с сервера (не из `.ipa`), PWA или sideload. Детали и матрица — + в §9; закрывать **до** кода обёртки. --- @@ -134,6 +139,13 @@ WebView (ядро web-only), но при этом теряется прямой > ресурс — сразу путь C на React Native (Expo) с WebView только под редактор. > Это сознательный размен «больше работы сейчас» за «более нативное ощущение». +⚠️ **Лицензионная оговорка к iOS.** Обычный Capacitor зашивает веб-билд +`apps/client` в `.ipa` — для публикации в App Store это **нарушает AGPL** +(см. §9). Выбор Capacitor для **Android** остаётся в силе, но на **iOS** +веб-клиент нельзя бандлить в бинарник: либо грузить его с сервера +(`server.url`), либо PWA. То есть рекомендация «B (Capacitor)» применима к +Android как есть, а к iOS — только в конфигурации без зашитого AGPL. + --- ## 6. Что доработать на бэкенде @@ -192,7 +204,109 @@ WebView (ядро web-only), но при этом теряется прямой --- -## 9. Оффлайн в будущем +## 9. Лицензионный блокер: AGPL ↔ App Store (iOS) + +> Это не инженерная, а **лицензионная** задача — закрывать её надо **до** кода +> обёртки, иначе можно сделать приложение, которое некуда легально опубликовать. +> Ниже — инженерно-лицензионный разбор, **не** юридическая консультация; финально +> подтверждать у того, кто разбирается в лицензиях. + +### 9.1. Суть конфликта + +gitmost — форк Docmost под **AGPL-3.0** (константа форка: «100% open, AGPL-only»). +Две вещи несовместимы: + +- **AGPLv3 §10** (последний абзац) запрещает накладывать на получателя кода + **любые дополнительные ограничения** сверх самой лицензии. +- **Стандартный EULA App Store** ровно их и накладывает: **FairPlay/DRM**, + привязка установки к Apple ID с лимитом устройств (**usage rules**), запрет + свободного перераспространения бинарника. + +Приняв условия Apple, чтобы попасть в App Store, вы нарушаете AGPL кода, который +раздаёте. + +### 9.2. Почему это бьёт именно по форку + +Запрет «дополнительных ограничений» связывает **лицензиатов, но не самого +правообладателя**: владелец 100% копирайта может опубликовать свой код в App Store. +Но в gitmost бóльшая часть копирайта принадлежит **upstream-Docmost** и +контрибьюторам — вы выступаете дистрибьютором *чужого* AGPL-кода и не можете +единолично добавить App-Store-исключение. + +Прецеденты: **VLC** (удалён из App Store в 2011 по жалобе на конфликт GPL с +условиями стора; вернулся только после перелицензирования и согласия +правообладателей), **GNU Go** — снят по той же причине. Это не теоретический риск. + +### 9.3. Ключевой принцип развязки: лицензия смотрит на `.ipa`, а не на устройство + +Определяющее — **что раздаёт сам Apple** (`.ipa` под FairPlay) и **кто раздаёт +AGPL-байты**, а не то, окажутся ли они в итоге на устройстве: + +- AGPL **внутри `.ipa`** → получен под ограничениями Apple → **нарушение**. +- AGPL **скачан с вашего сервера** → получен от вас под AGPL (исходники открыты, + §13 выполнен) → ограничения Apple на него **не** накладываются, даже если бандл + кэшируется в песочнице приложения. + +Следствие: **офлайн на iOS легально достижим** — если кэшированный бандл пришёл с +вашего сервера, а не из `.ipa`. Ограничение тут не лицензионное, а в **ревью +Apple** (см. §9.5). + +### 9.4. Варианты «грузить веб-клиент с сервера» + +**A. WebView навигируется на хостед-клиент (`server.url`).** Capacitor умеет +`server: { url: 'https://app.example.com' }` — оболочка грузит WebView с удалённого +URL, мост и нативные плагины по-прежнему инжектятся. В `.ipa` — ноль AGPL. + +- Плюс: лицензионно самый чистый; **origin = ваш домен**, поэтому cookie/CORS + работают как в браузере (боль `capacitor://localhost` ↔ API из §6 исчезает — + токен в body/Keychain может и не понадобиться). +- Минус: холодный старт требует сети; сервер лёг → приложение кирпич; офлайна по + умолчанию нет. + +**B. OTA: пустой шелл скачивает и кэширует бандл.** Шелл при первом запуске тянет +JS-бандл с вашего сервера и кэширует как веб-ассеты (механизм Cordova/CodePush). +Open-source self-host-вариант — `@capgo/capacitor-updater` (важно для AGPL-проекта: +без привязки к проприетарному Appflow). + +- Плюс: **даёт офлайн** — кэш AGPL легален, т.к. распространён вами, а не Apple. +- Минус: упирается в политику Apple по hot-update (§9.5). + +**Не-обходы (мифы):** «никто не засудит» — это нарушение, а не обход; «LGPL-нуть +обёртку» — не помогает (проблема в AGPL-веб-клиенте, а не в обёртке); «mere +aggregation» — не катит: зашитый бандл это комбинированное распространяемое +произведение, а не простая агрегация. + +### 9.5. Гейты Apple + +| # | Guideline | Суть | Влияние | +|---|---|---|---| +| 1 | **2.5.2** (исполняемый код) | Скачивать/исполнять **нативный** код нельзя, **но** есть исключение для скриптов, исполняемых встроенным WebKit/JavascriptCore, если они не меняют назначение приложения | Загрузка веб-клиента в `WKWebView` под исключение попадает: вариант A — чистый, B — терпимый, но с границами | +| 2 | **4.2** (minimum functionality) | Чистый WebView-«просто сайт» рискует отклонением | Лечится нативной ценностью в оболочке (push/APNs, биометрия, share, файлы — ваш нативный код, не AGPL) | +| 3 | конфликт двух гейтов | «Лицензионно чистый» вариант (пустой шелл качает всё) — самый рискованный для ревью; «безопасный для ревью» (зашить веб-билд в `.ipa`) — лицензионное нарушение | **Совместить (офлайн) + (чистая AGPL) + (низкий риск ревью) в одной конфигурации нельзя — выбираете любые два** | + +Безопасность: раз исполняете удалённый код — только HTTPS, желательно cert-pinning +(подмена сервера = произвольный JS в WebView пользователя). + +### 9.6. Итоговая матрица распространения iOS + +| Конфигурация | AGPL-чистота | Офлайн | Риск ревью Apple | +|---|---|---|---| +| A. `server.url` на хостед-клиент | ✅ чистая | ❌ нет | средний (4.2, лечится плагинами) | +| B. OTA пустой шелл + кэш бандла | ✅ чистая | ✅ есть | выше (2.5.2 + 4.2) | +| Зашить веб-билд в `.ipa` (обычный Capacitor) | ❌ нарушение | ✅ | низкий | +| **PWA** | ✅ чистая | ✅ | App Store не нужен | +| Sideload / EU DMA-маркетплейсы (iOS 17.4+) | ✅ чистая | ✅ | вне App Store; **только ЕС** | + +**Вывод:** для iOS **PWA** — самое дешёвое решение, закрывающее всё сразу. Если +присутствие именно в App Store критично — **вариант A** (`server.url` + нативные +плагины под 4.2) легальный и реалистичный ценой «онлайн для холодного старта». +Офлайн в App Store (вариант B) технически и лицензионно возможен, но это +максимальный риск на ревью — закладывать только если офлайн на iOS обязателен. +Совместить «App Store + зашитый офлайн AGPL» легально нельзя, пока копирайт не ваш. + +--- + +## 10. Оффлайн в будущем Оффлайн сейчас не требуется, но позиция хорошая: @@ -208,7 +322,7 @@ WebView (ядро web-only), но при этом теряется прямой --- -## 10. Открытые вопросы (зафиксировать до старта) +## 11. Открытые вопросы (зафиксировать до старта) - **Q1.** Путь: Capacitor (B) с эволюцией в гибрид, или сразу React Native (C)? Рекомендация — B. @@ -218,14 +332,23 @@ WebView (ядро web-only), но при этом теряется прямой - **Q4.** Подключать ли OpenAPI/Swagger для генерации мобильного клиента? - **Q5.** Когда включать оффлайн (M0…M4 из offline-sync-plan.md) относительно первого мобильного релиза? +- **Q6.** iOS-дистрибуция при AGPL (§9): App Store через `server.url` + (онлайн-клиент, без зашитого AGPL), PWA или sideload/EU-маркетплейсы? Этот + лицензионный путь нужно подтвердить **до** кода обёртки. Рекомендация — PWA для + iOS, Capacitor для Android. --- -## 11. Чеклист первого шага (бутстрап Capacitor, iOS-first) +## 12. Чеклист первого шага (бутстрап Capacitor, iOS-first) +- [ ] **Закрыть лицензионный путь iOS (§9) ДО кода обёртки:** выбрать + `server.url` / PWA / sideload и подтвердить у разбирающегося в лицензиях. +- [ ] **Не бандлить AGPL-веб-клиент в iOS `.ipa`** (DRM/usage-rules App Store ⟂ + AGPLv3 §10) — на iOS грузить клиент с сервера или идти через PWA. - [ ] Прогнать существующий адаптивный UI как PWA/в WebView, отловить отличия (жесты, IME в редакторе, safe-area). -- [ ] Добавить Capacitor в монорепо, нацелить на веб-билд `apps/client`. +- [ ] Добавить Capacitor в монорепо, нацелить на веб-билд `apps/client` + (Android — зашитый билд; iOS — `server.url`/PWA без зашитого AGPL, см. §9). - [ ] `npx cap add ios` (Android — `npx cap add android`, когда будет готова обвязка). - [ ] Бэкенд: мобильный логин-флоу с токеном в body; хранить токен в Keychain/ Keystore; слать `Authorization: Bearer`.