feat(mobile): bootstrap mobile app (PWA + Capacitor + backend auth/CORS) #116
Closed
Ghost
wants to merge 2 commits from
feature/mobile-app-bootstrap into develop
pull from: feature/mobile-app-bootstrap
merge into: vvzvlad:develop
vvzvlad:main
vvzvlad:test/244-part-b
vvzvlad:fix/255-ws-redis-adapter-leak
vvzvlad:feat/251-intentional-clear
vvzvlad:fix/252-e2e-open-handles
vvzvlad:feat/184-autonomous-agent-runs
vvzvlad:feat/221-image-captions
vvzvlad:feat/git-sync
vvzvlad:refactor/193-tool-spec-registry
vvzvlad:fix/244-dataloss-bugs
vvzvlad:fix/embeddings-reindex-progress
vvzvlad:develop
vvzvlad:feature/offline-sync
vvzvlad:feat/229-catalog-yaml
vvzvlad:feat/243-blob-sandbox
vvzvlad:feat/228-inline-footnotes
vvzvlad:fix/qa-ui-bugs-216-218
vvzvlad:feature/agent-roles-catalog
vvzvlad:fix/share-alias-rename
vvzvlad:fix/ai-chat-empty-render
vvzvlad:feat/191-chat-doc-binding
vvzvlad:feat/201-temporary-notes
vvzvlad:feat/198-interrupt-agent
vvzvlad:feat/ai-chat-full-history
vvzvlad:feat/199-ai-generate-title
vvzvlad:feat/205-share-aliases
vvzvlad:batch/issues-189-187-170
vvzvlad:feat/170-mcp-test-button
vvzvlad:feat/189-context-badge
vvzvlad:feat/198-interrupt-agent-send-now
vvzvlad:fix/issues-190-159
vvzvlad:fix/ai-chat-new-chat-during-stream
vvzvlad:fix/ai-chat-stream-perf
vvzvlad:batch/issues-2026-06-25
vvzvlad:feat/ai-chat-persistent-history
vvzvlad:fix/ai-chat-copy-chat-wysiwyg
vvzvlad:fix/ai-stream-reset-resilience
vvzvlad:fix/ai-stream-undici-timeout
vvzvlad:fix/footnote-review-1227-followup
vvzvlad:fix/ai-chat-token-counter-realtime
vvzvlad:docs/manual-qa-test-plan
No Reviewers
Labels
Clear labels
bug
documentation
duplicate
enhancement
epic
feature
good first issue
help wanted
idea
invalid
needs-human
question
refactor
review/approved
review/changes-requested
review/needs
security
status/blocked
status/done
status/in-progress
status/ready
test
wontfix
Something isn't working
Improvements or additions to documentation
This issue or pull request already exists
New feature or request
Large multi-phase effort spanning many changes
New functionality request
Good for newcomers
Extra attention is needed
Idea / proposal for discussion
This doesn't seem right
эскалация: нужно решение человека
Further information is requested
Code cleanup / refactoring
в последнем ревью нет открытых blocking-находок
последнее ревью оставило открытые blocking-находки
head не ревьюился (head != reviewed_head)
Security / hardening issue
ждёт зависимость blocked_by
закрыто и проверено
в активной работе (мягкая заявка)
специфицировано, не заблокировано, ждёт исполнителя
Test coverage / test infrastructure
This will not be worked on
No Label
Milestone
No items
No Milestone
Projects
Clear projects
No project
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: vvzvlad/gitmost#116
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking 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