feat(mobile): bootstrap mobile app (PWA + Capacitor + backend auth/CORS) #116
Reference in New Issue
Block a user
Delete Branch "feature/mobile-app-bootstrap"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Что это
Бутстрап мобильного приложения по чеклисту §12 из docs/mobile-app-plan.md. Реализуемая сейчас часть: бэкенд-задел §6 + PWA + конфиг Capacitor. Нативные платформы, APNs/FCM и публикация остаются ручными шагами (требуют Xcode/внешних аккаунтов) и описаны в
docs/mobile-bootstrap.md.Бэкенд (§6)
returnTokenв логине возвращает JWT в теле (data.authToken, т.к. ответы оборачиваются интерсептором) — для хранения в Keychain/Keystore и отправки какAuthorization: Bearer. Веб-флоу через httpOnly-cookie не меняется (opt-in).app.enableCors()— явный allowlist (APP_URL+CORS_ALLOWED_ORIGINS+ WebView-origin'ы Capacitor),credentials: true./api/docsза флагомSWAGGER_ENABLED.CORS_ALLOWED_ORIGINS,SWAGGER_ENABLED,CAP_SERVER_URL.PWA
manifest.json; добавлен hand-rolled service worker (network-first для навигаций, stale-while-revalidate для статики, никогда не трогает/api,/socket.io,/collab); production-only регистрация;apple-touch-icon. Безvite-plugin-pwa(rolldown-vite 8).Capacitor
capacitor.config.ts(webDirapps/client/dist; iOS — черезCAP_SERVER_URL/server.url, чтобы не бандлить AGPL-клиент в .ipa, см. §9 плана); скриптыcap:*/mobile:build, зависимости@capacitor/*,.gitignoreдля нативных каталогов.docs/mobile-bootstrap.md— что сделано и оставшиеся ручные шаги.Как проверялось
Реализовано параллельными субагентами в отдельном worktree, прогнан агент-ревьюер по полному diff (только low/информационные замечания, ключевые исправлены). JSON-файлы валидны,
sw.jsпроходитnode --check. Сборка/pnpm install/тесты не запускались (в worktree нет node_modules) — это локальный шаг.🤖 Generated with Claude Code
Code review — PR #116: мобильный bootstrap (PWA + Capacitor + auth/CORS)
Вердикт: Request changes (нужны доработки).
Изменение спроектировано аккуратно и по делу, но к merge мешают мелкие, быстро правимые вещи: новая логика (
returnTokenвlogin(), парсеры env, влияющие на CORS/Swagger) идёт без тестов; два изменённых места не проходятprettier; ломающее изменение поведения CORS (open → allowlist) не отражено вCHANGELOG. Криминала нет.Что ревьюилось: дифф PR #116 (
feature/mobile-app-bootstrap→develop), 14 файлов, +288/−23. Рабочее дерево репозитория сейчас содержит более поздние правки (offline /vite-plugin-pwa), которые в этот PR не входят — ревью сделано строго по диффу PR относительноdevelop.Critical
Нет.
Warnings
returnTokenвlogin()без тестов —apps/server/src/core/auth/auth.controller.ts:101-104,114-116. Единственный спек контроллера — construct-only smoke-тест, поэтому не зафиксировано ключевое свойство: токен попадает в тело ответа только приreturnToken, а по умолчанию его там быть не должно (ровно то, что комментарии называют недопустимым для веб-клиента). Регрессия здесь молча утекла бы токеном в body. Fix: спек наlogin()со стабами —returnToken:true→{ authToken }(+cookie); без флага →undefined(+cookie, без токена в теле).getCorsAllowedOrigins()/isSwaggerEnabled()без тестов —apps/server/src/integrations/environment/environment.service.ts:324-338. Проект тестирует ровно такой парсинг (trust-proxy.util.spec.ts: пусто/пробелы/регистр/границы), аenvironment.service.spec.ts— пустой smoke. Значения напрямую кормят CORS-allowlist и выдачу Swagger, т.е. ошибка парсинга = security-эффект (слишком широкий allowlist / случайно открытые доки). Fix: кейсы на''/один/несколько/"a,, ,b"/пробелы и'true'/'TRUE'/'false'/unset/мусор.CHANGELOGне обновлён —CHANGELOG.md([Unreleased]). Не отражены новые env (CORS_ALLOWED_ORIGINS,SWAGGER_ENABLED,CAP_SERVER_URL), эндпоинт/api/docs, флагreturnToken, и главное — смена CORS с открытогоapp.enableCors()на явный allowlist. Последнее — Changed/Breaking для self-hosters: ранее работавшие кросс-доменные клиенты теперь отклоняются, пока не добавлены вCORS_ALLOWED_ORIGINS. Fix: добавить записи в[Unreleased](Added + Changed/Breaking про CORS).prettier --checkпадает на двух изменённых местах —apps/server/src/main.ts:163(origin-колбэк, 101 симв.) иapps/server/src/core/auth/dto/login.dto.ts:1(импортclass-validator, 87 симв.) приprintWidth=80(apps/server/.prettierrc). Проверено прогономprettier. Fix:pnpm --filter server format/prettier --write.Suggestions
http://localhost,https://localhost,capacitor://localhost,ionic://localhost+credentials:trueдаже на чисто-веб-деплое без мобильного шелла (apps/server/src/main.ts:150-160). Сегодня не эксплуатируется:authToken-cookiehttpOnly+SameSite=lax, все мутации — POST (RPC-стиль), кросс-доменная авторизация только поBearer-токену, которого у чужой страницы нет. Это defense-in-depth: если когда-нибудь ослабятSameSite(например ради embedding), эти origin'ы сразу станут дырой. Fix: добавлять capacitor/ionic/localhost только когда реально включён мобильный режим (флаг / наличиеCAP_SERVER_URL), остальное гнать черезCORS_ALLOWED_ORIGINS.APP_URL: axiosbaseURL:"/api",distраздаётся тем же сервером; WS-gateway и collab-сервер имеют отдельные CORS-конфиги и не затронуты). Ломается только внешний, отдельно хостящийся кросс-доменный клиент существующего оператора. Усиление намеренное, escape-hatch —CORS_ALLOWED_ORIGINS; достаточно отметить вCHANGELOG(см. выше).apps/client/public/sw.js): ручнойCACHE_VERSION+ неограниченный рост кэша в пределах одной версии (старые хэш-бандлы не вычищаются до ручного бампа"gitmost-v1"). Замечу для точности: классической «протухшей оболочки на каждый деплой» здесь нет — навигации идут network-first (свежийindex.htmlonline), кэшированный/отдаётся только офлайн. Это гигиена кэша, не баг. Fix: выводить версию из сборки (APP_VERSION/хэш манифеста), чтобыactivate-очистка срабатывала на каждый деплой.sw.js(NETWORK_ONLY_PREFIXES, navigate vs asset, проверка кэшируемостиstatus===200 && type==="basic") содержит реальные ветки, но какpublic/-скрипт не импортируется и не покрыта. Низкий приоритет (bootstrap). При желании — вынести предикатыisNetworkOnly()/isCacheable()в импортируемый модуль и юнит-тестировать.docs/mobile-app-plan.md§2.4 устарел — описание «как есть» теперь неверно: «токен не возвращается в теле», «CORS открыт без конфигурацииapp.enableCors()(L144)», «Swagger отсутствует» — все три ложны после этого PR; плюс устаревшие номера строк и неотмеченные пункты чек-листа §12. Fix: обновить §2.4 (или пометить «до bootstrap» и сослаться наdocs/mobile-bootstrap.md) и отметить выполненные пункты §12.Test coverage
Новой логики без тестов: (1) ветки
returnTokenвlogin(); (2)getCorsAllowedOrigins/isSwaggerEnabled. Обе — first-class пробелы (см. Warnings), особенно из-за security-чувствительности.sw.js— по желанию. Остальной дифф (manifest.json,capacitor.config.ts,package.json,.env.example, docs) тестов не требует.Architecture & design (forward-looking, non-blocking)
main.ts. Сейчас ~30 строк allowlist + Swagger живут прямо в раздувающемсяbootstrap(), а origin-решение нельзя юнит-тестировать в изоляции.bootstrap()растёт, allowlist нетестируем.buildCorsOptions(environmentService)/ предикатresolveCorsOrigin(allowlist)рядом сresolve-frame-header/trust-proxy.util.ts(effort: small). Плюсы: повторяет уже принятый в этом файле паттернresolveFrameHeader/resolveTrustProxy, даёт точку юнит-теста для allowlist, который мобильный roadmap будет дорабатывать. Минусы: ещё один маленький файл при одном вызывающем.environment.service.ts.getCorsAllowedOriginsдословно дублируетgetIframeAllowedOrigins(split/trim/filter), а boolean-ридерыtoLowerCase()==='true'повторяются ~30 раз.getList(key)/getBool(key, def), публичные геттеры делегируют (effort: small для новых/соседних, medium по всему файлу): одна точка правки парсинга; минус — churn в стабильном файле.Примечание: «конфликт двух service worker'ов» — ложное срабатывание для этого PR
Автоматический проход пометил, что hand-rolled
sw.jsконфликтует сvite-plugin-pwa(generateSW) и «переизобретает уже подключённую зависимость» (изначально — как critical). Проверка по голове ветки PR (gitea/feature/mobile-app-bootstrap) и поdevelop:vite-plugin-pwaтам нет ни вapps/client/package.json, ни вvite.config.ts— он есть только в более позднем рабочем дереве (post-merge reconciliation). Поэтому для PR #116 относительноdevelopэто не дефект, а ложное срабатывание, и hand-rolled SW (~80 строк без тяжёлой зависимости) здесь — оправданный выбор. Forward-looking: если позже вводитьvite-plugin-pwa, нужно убрать ручнойsw.js+ его регистрацию вmain.tsx, иначе два SW подерутся за/sw.js(что и пришлось разруливать после merge).Сгенерировано оркестратором мульти-аспектного ревью (security · stability · conventions · documentation · regressions · test-coverage · simplification · architecture), дедупликация и проверка findings по исходникам — координатором.
Red-team отчёт — PR #116 (под реальную модель угроз) — 2026-06-21
0. Модель угроз (с твоих слов)
Локальный бложек, ~5 доверенных людей; снаружи целенаправленно не атакуют, DoS не рассматриваем.
«Кража токенов» (чтобы аноним не жёг платные LLM-токены) — единственное, что по-настоящему волнует, но
это про подсистему ассистента, а её PR #116 не трогает (см. §3). Поэтому здесь — оценка именно
изменений #116 под эту линзу: не параноидально, по делу.
1. Вывод одной строкой
В PR #116 серьёзного под твою модель угроз нет. Всё, что меняется — низкий риск. Есть 3 мелочи
гигиены и одна приятная деталь (PWA-кэш приватных данных не сохраняет). Можно мержить; пункты ниже —
по желанию.
2. Что меняет PR #116 и насколько это важно
returnToken: JWT в теле ответаauth.controller.ts,login.dto.tslocalhost+credentials:true+!originразрешёнmain.ts/api/docsбез авторизации (если включить)main.ts,environment.service.tsSWAGGER_ENABLED=falseв продеpublic/sw.js,main.tsxCACHE_VERSIONна деплое2.1
returnToken— JWT в теле ответа · lowapps/server/src/core/auth/auth.controller.ts:99-119по флагуreturnTokenотдаёт access-token в теле(
data.authToken) в дополнение к httpOnly-куке — чтобы мобильный клиент хранил его в Keychain и слал какAuthorization: Bearer. Это опт-ин: веб-клиент флаг не шлёт и остаётся на httpOnly-куке; токенотзываемый (сессия сверяется на каждом запросе). Для 5 доверенных людей риск «XSS украдёт токен из тела»
низкий. Единственное правило гигиены: веб-клиент не должен запрашивать
returnToken(тогда токенникогда не лежит в JS-доступном месте в браузере). Сопутствующее: TTL токена 90 дней без ротации —
к сведению, актуально только когда токен живёт в мобильном хранилище.
2.2 CORS — хардкод
localhost+credentials:true· lowapps/server/src/main.ts:146-188: allowlist намертво включаетhttp://localhost,https://localhost(+
capacitor://localhost,ionic://localhost) и ставитcredentials:true; запросы без заголовкаOrigin(!origin) всегда разрешены. Теоретический риск — кросс-origin доступ с чего-то, что отдаётсяровно с
http://localhost:80на машине жертвы; для доверенной группы это почти невероятно.!originне эксплуатируется из браузера (браузер всегда шлёт Origin при кросс-origin). По желанию: на чисто
веб-инсталляции убрать localhost-записи (они нужны только нативной сборке) — но это косметика.
2.3 Swagger без guard · low
apps/server/src/main.ts:178-187: приSWAGGER_ENABLED=trueстраницы/api/docsи/api/docs-jsonдоступны без авторизации — это карта всех эндпойнтов. Это не дыра (эндпойнты по-прежнему за
авторизацией), но публиковать карту незачем. По умолчанию выключено — просто держи
SWAGGER_ENABLEDвыключенным в проде, включай только локально для генерации мобильного клиента.
2.4 PWA service worker · low + приятная деталь
apps/client/public/sw.js(новый, рукописный) сделан консервативно и аккуратно:/api,/socket.io,/collab— всегда сеть (sw.js:9,49-51). Значитникакие приватные данные в кэш не попадают (всё пользовательское идёт через
/api). Это важноеотличие от более крупного PR #120, где офлайн-кэш писал приватный контент в IndexedDB; здесь этого
нет. Под твою модель — хорошо.
только same-origin 200 basic-ответы (
sw.js:54-80).window load(main.tsx:65-74).Единственная операционная заметка:
CACHE_VERSION = "gitmost-v1"фиксированный (sw.js:6). Vite даётассетам хэш-имена, так что устаревшие чанки не залипнут; но не-хэшированные ассеты по SWR могут
отдаваться из кэша до фоновой ревалидации. Если на деплое что-то «залипает» — бампни
CACHE_VERSION(в коде так и написано). Это не безопасность, а удобство.
Capacitor-конфиг (
capacitor.config.ts) —cleartext:false, iOS грузит клиент с твоего сервера черезCAP_SERVER_URL(чтобы не бандлить AGPL-клиент в .ipa). Замечаний нет.3. Про «кражу токенов» (явно, чтобы закрыть тему)
Кража LLM-токенов происходит через подсистему анонимного ассистента (
core/ai-chat/public-share-chat.*).PR #116 этот код не меняет — в диффе его нет. Поэтому в рамках ревью именно #116 эта тема неприменима.
Если хочешь отдельный разбор ассистента (он выключен по умолчанию и защищён лимитами) — это отдельная
задача, не этот PR; здесь я её намеренно не тяну.
4. Чего тут можно НЕ бояться
/api) SW не кэширует (§2.4).sameSite:laxотсекаетпроизвольные кросс-сайты. Остаточный риск сведён к co-tenancy на самом
localhost(§2.2), что длядоверенной группы нерелевантно.
5. Охват и проверки
main.ts,auth.controller.ts,login.dto.ts,environment.service.ts; client:sw.js,main.tsx,manifest.json,index.html;capacitor.config.ts,.env.example).returnTokenопт-ин и токен отзываемый; CORS включает localhost +credentials;Swagger при включении без guard; SW не трогает
/api//socket.io//collabи не кэширует приватныеответы; регистрация SW только в проде.
Pull request closed