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

Что это

Бутстрап мобильного приложения по чеклисту §12 из docs/mobile-app-plan.md. Реализуемая сейчас часть: бэкенд-задел §6 + PWA + конфиг Capacitor. Нативные платформы, APNs/FCM и публикация остаются ручными шагами (требуют Xcode/внешних аккаунтов) и описаны в docs/mobile-bootstrap.md.

Бэкенд (§6)

  • auth: опциональный флаг returnToken в логине возвращает JWT в теле (data.authToken, т.к. ответы оборачиваются интерсептором) — для хранения в Keychain/Keystore и отправки как Authorization: Bearer. Веб-флоу через httpOnly-cookie не меняется (opt-in).
  • CORS: вместо открытого app.enableCors() — явный allowlist (APP_URL + CORS_ALLOWED_ORIGINS + WebView-origin'ы Capacitor), credentials: true.
  • Swagger: опциональный OpenAPI на /api/docs за флагом SWAGGER_ENABLED.
  • env: добавлены 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 (webDir apps/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

## Что это Бутстрап мобильного приложения по чеклисту §12 из [docs/mobile-app-plan.md](docs/mobile-app-plan.md). Реализуемая сейчас часть: бэкенд-задел §6 + PWA + конфиг Capacitor. Нативные платформы, APNs/FCM и публикация остаются ручными шагами (требуют Xcode/внешних аккаунтов) и описаны в `docs/mobile-bootstrap.md`. ## Бэкенд (§6) - **auth**: опциональный флаг `returnToken` в логине возвращает JWT в теле (`data.authToken`, т.к. ответы оборачиваются интерсептором) — для хранения в Keychain/Keystore и отправки как `Authorization: Bearer`. Веб-флоу через httpOnly-cookie не меняется (opt-in). - **CORS**: вместо открытого `app.enableCors()` — явный allowlist (`APP_URL` + `CORS_ALLOWED_ORIGINS` + WebView-origin'ы Capacitor), `credentials: true`. - **Swagger**: опциональный OpenAPI на `/api/docs` за флагом `SWAGGER_ENABLED`. - **env**: добавлены `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` (webDir `apps/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](https://claude.com/claude-code)
Ghost added 1 commit 2026-06-21 14:11:35 +03:00
Implements the §12 bootstrap from docs/mobile-app-plan.md.

Backend (§6):
- auth: optional returnToken flag on login returns the JWT in the body
  (data.authToken) for native Keychain/Keystore + Bearer; web cookie flow
  unchanged.
- main.ts: explicit CORS allowlist (APP_URL + CORS_ALLOWED_ORIGINS env +
  Capacitor WebView origins), credentials enabled, replaces open enableCors().
- optional OpenAPI/Swagger at /api/docs behind SWAGGER_ENABLED.
- env: CORS_ALLOWED_ORIGINS, SWAGGER_ENABLED, CAP_SERVER_URL.

PWA:
- manifest metadata, hand-rolled service worker (network-first nav, SWR
  assets, never intercepts /api,/socket.io,/collab), prod-only registration,
  apple-touch-icon.

Capacitor:
- capacitor.config.ts (webDir apps/client/dist; iOS via CAP_SERVER_URL to
  avoid bundling the AGPL client in the .ipa, see plan §9), cap:* scripts,
  deps, .gitignore for native dirs.
- docs/mobile-bootstrap.md documenting what is done and the remaining manual
  steps (cap add ios/android, APNs/FCM, stores).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Owner

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-bootstrapdevelop), 14 файлов, +288/−23. Рабочее дерево репозитория сейчас содержит более поздние правки (offline / vite-plugin-pwa), которые в этот PR не входят — ревью сделано строго по диффу PR относительно develop.

Critical

Нет.

Warnings

  • [test coverage] Ветки 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, без токена в теле).
  • [test coverage] Новые env-парсеры 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/мусор.
  • [documentation] 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).
  • [conventions] 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

  • [security] CORS-allowlist безусловно доверяет http://localhost, https://localhost, capacitor://localhost, ionic://localhost + credentials:true даже на чисто-веб-деплое без мобильного шелла (apps/server/src/main.ts:150-160). Сегодня не эксплуатируется: authToken-cookie httpOnly+SameSite=lax, все мутации — POST (RPC-стиль), кросс-доменная авторизация только по Bearer-токену, которого у чужой страницы нет. Это defense-in-depth: если когда-нибудь ослабят SameSite (например ради embedding), эти origin'ы сразу станут дырой. Fix: добавлять capacitor/ionic/localhost только когда реально включён мобильный режим (флаг / наличие CAP_SERVER_URL), остальное гнать через CORS_ALLOWED_ORIGINS.
  • [regressions] Та же смена CORS (open → allowlist) — для in-repo веб-клиента это no-op (он same-origin с APP_URL: axios baseURL:"/api", dist раздаётся тем же сервером; WS-gateway и collab-сервер имеют отдельные CORS-конфиги и не затронуты). Ломается только внешний, отдельно хостящийся кросс-доменный клиент существующего оператора. Усиление намеренное, escape-hatch — CORS_ALLOWED_ORIGINS; достаточно отметить в CHANGELOG (см. выше).
  • [stability] Service worker (apps/client/public/sw.js): ручной CACHE_VERSION + неограниченный рост кэша в пределах одной версии (старые хэш-бандлы не вычищаются до ручного бампа "gitmost-v1"). Замечу для точности: классической «протухшей оболочки на каждый деплой» здесь нет — навигации идут network-first (свежий index.html online), кэшированный / отдаётся только офлайн. Это гигиена кэша, не баг. Fix: выводить версию из сборки (APP_VERSION/хэш манифеста), чтобы activate-очистка срабатывала на каждый деплой.
  • [test coverage] Маршрутизация в sw.js (NETWORK_ONLY_PREFIXES, navigate vs asset, проверка кэшируемости status===200 && type==="basic") содержит реальные ветки, но как public/-скрипт не импортируется и не покрыта. Низкий приоритет (bootstrap). При желании — вынести предикаты isNetworkOnly()/isCacheable() в импортируемый модуль и юнит-тестировать.
  • [documentation] 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)

  • CORS/bootstrap в main.ts. Сейчас ~30 строк allowlist + Swagger живут прямо в раздувающемся bootstrap(), а origin-решение нельзя юнит-тестировать в изоляции.
    • A — оставить инлайн (effort: none). Плюсы: минимальный дифф, всё рядом. Минусы: bootstrap() растёт, allowlist нетестируем.
    • B — вынести чистый helper buildCorsOptions(environmentService) / предикат resolveCorsOrigin(allowlist) рядом с resolve-frame-header/trust-proxy.util.ts (effort: small). Плюсы: повторяет уже принятый в этом файле паттерн resolveFrameHeader/resolveTrustProxy, даёт точку юнит-теста для allowlist, который мобильный roadmap будет дорабатывать. Минусы: ещё один маленький файл при одном вызывающем.
    • C — отдельный Nest config/provider для security-конфига (effort: medium). Плюсы: единый дом для cross-cutting security. Минусы: overkill для bootstrap.
    • Рекомендация: B. Не блокер.
  • env-парсинг в environment.service.ts. getCorsAllowedOrigins дословно дублирует getIframeAllowedOrigins (split/trim/filter), а boolean-ридеры toLowerCase()==='true' повторяются ~30 раз.
    • A — как есть (effort: none): идеально консистентно с файлом, нулевой churn; минус — дублирование парсинга.
    • B — приватные getList(key) / getBool(key, def), публичные геттеры делегируют (effort: small для новых/соседних, medium по всему файлу): одна точка правки парсинга; минус — churn в стабильном файле.
    • Рекомендация: без сильного предпочтения; для bootstrap-PR — A, helper отдельным follow-up. Не блокер.

Примечание: «конфликт двух 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 по исходникам — координатором.

## 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 - **[test coverage] Ветки `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, без токена в теле). - **[test coverage] Новые env-парсеры `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/мусор. - **[documentation] `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). - **[conventions] `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 - **[security] CORS-allowlist безусловно доверяет `http://localhost`, `https://localhost`, `capacitor://localhost`, `ionic://localhost` + `credentials:true`** даже на чисто-веб-деплое без мобильного шелла (`apps/server/src/main.ts:150-160`). Сегодня **не** эксплуатируется: `authToken`-cookie `httpOnly`+`SameSite=lax`, все мутации — POST (RPC-стиль), кросс-доменная авторизация только по `Bearer`-токену, которого у чужой страницы нет. Это defense-in-depth: если когда-нибудь ослабят `SameSite` (например ради embedding), эти origin'ы сразу станут дырой. *Fix:* добавлять capacitor/ionic/localhost только когда реально включён мобильный режим (флаг / наличие `CAP_SERVER_URL`), остальное гнать через `CORS_ALLOWED_ORIGINS`. - **[regressions] Та же смена CORS (open → allowlist)** — для in-repo веб-клиента это no-op (он same-origin с `APP_URL`: axios `baseURL:"/api"`, `dist` раздаётся тем же сервером; WS-gateway и collab-сервер имеют отдельные CORS-конфиги и не затронуты). Ломается только внешний, отдельно хостящийся кросс-доменный клиент существующего оператора. Усиление намеренное, escape-hatch — `CORS_ALLOWED_ORIGINS`; достаточно отметить в `CHANGELOG` (см. выше). - **[stability] Service worker (`apps/client/public/sw.js`): ручной `CACHE_VERSION` + неограниченный рост кэша** в пределах одной версии (старые хэш-бандлы не вычищаются до ручного бампа `"gitmost-v1"`). Замечу для точности: классической «протухшей оболочки на каждый деплой» здесь **нет** — навигации идут network-first (свежий `index.html` online), кэшированный `/` отдаётся только офлайн. Это гигиена кэша, не баг. *Fix:* выводить версию из сборки (`APP_VERSION`/хэш манифеста), чтобы `activate`-очистка срабатывала на каждый деплой. - **[test coverage] Маршрутизация в `sw.js`** (`NETWORK_ONLY_PREFIXES`, navigate vs asset, проверка кэшируемости `status===200 && type==="basic"`) содержит реальные ветки, но как `public/`-скрипт не импортируется и не покрыта. Низкий приоритет (bootstrap). При желании — вынести предикаты `isNetworkOnly()`/`isCacheable()` в импортируемый модуль и юнит-тестировать. - **[documentation] `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) - **CORS/bootstrap в `main.ts`.** Сейчас ~30 строк allowlist + Swagger живут прямо в раздувающемся `bootstrap()`, а origin-решение нельзя юнит-тестировать в изоляции. - *A — оставить инлайн* (effort: none). Плюсы: минимальный дифф, всё рядом. Минусы: `bootstrap()` растёт, allowlist нетестируем. - *B — вынести чистый helper* `buildCorsOptions(environmentService)` / предикат `resolveCorsOrigin(allowlist)` рядом с `resolve-frame-header`/`trust-proxy.util.ts` (effort: small). Плюсы: повторяет уже принятый в этом файле паттерн `resolveFrameHeader`/`resolveTrustProxy`, даёт точку юнит-теста для allowlist, который мобильный roadmap будет дорабатывать. Минусы: ещё один маленький файл при одном вызывающем. - *C — отдельный Nest config/provider для security-конфига* (effort: medium). Плюсы: единый дом для cross-cutting security. Минусы: overkill для bootstrap. - **Рекомендация:** B. Не блокер. - **env-парсинг в `environment.service.ts`.** `getCorsAllowedOrigins` дословно дублирует `getIframeAllowedOrigins` (split/trim/filter), а boolean-ридеры `toLowerCase()==='true'` повторяются ~30 раз. - *A — как есть* (effort: none): идеально консистентно с файлом, нулевой churn; минус — дублирование парсинга. - *B — приватные `getList(key)` / `getBool(key, def)`*, публичные геттеры делегируют (effort: small для новых/соседних, medium по всему файлу): одна точка правки парсинга; минус — churn в стабильном файле. - **Рекомендация:** без сильного предпочтения; для bootstrap-PR — A, helper отдельным follow-up. Не блокер. ### Примечание: «конфликт двух 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). --- <sub>Сгенерировано оркестратором мульти-аспектного ревью (security · stability · conventions · documentation · regressions · test-coverage · simplification · architecture), дедупликация и проверка findings по исходникам — координатором.</sub>
Owner

Red-team отчёт — PR #116 (под реальную модель угроз) — 2026-06-21

Цель: только PR #116feat(mobile): bootstrap mobile app (PWA + Capacitor + backend auth/CORS)
(https://gitea.vvzvlad.xyz/vvzvlad/gitmost/pulls/116, ветка feature/mobile-app-bootstrap).
Разбирается ровно дифф этого PR (14 файлов): returnToken в логине, явный CORS, опциональный Swagger,
PWA service worker + Capacitor-конфиг. Метод: read-only.

0. Модель угроз (с твоих слов)

Локальный бложек, ~5 доверенных людей; снаружи целенаправленно не атакуют, DoS не рассматриваем.
«Кража токенов» (чтобы аноним не жёг платные LLM-токены) — единственное, что по-настоящему волнует, но
это про подсистему ассистента, а её PR #116 не трогает (см. §3). Поэтому здесь — оценка именно
изменений #116 под эту линзу: не параноидально, по делу.

1. Вывод одной строкой

В PR #116 серьёзного под твою модель угроз нет. Всё, что меняется — низкий риск. Есть 3 мелочи
гигиены и одна приятная деталь (PWA-кэш приватных данных не сохраняет). Можно мержить; пункты ниже —
по желанию.

2. Что меняет PR #116 и насколько это важно

# Изменение Файл severity (под твою модель) действие
1 returnToken: JWT в теле ответа auth.controller.ts, login.dto.ts low держать веб-клиент на куке
2 CORS: хардкод localhost + credentials:true + !origin разрешён main.ts low опц. убрать localhost в проде
3 Swagger /api/docs без авторизации (если включить) main.ts, environment.service.ts low SWAGGER_ENABLED=false в проде
4 PWA service worker (рукописный) public/sw.js, main.tsx low (+плюс) по желанию бампать CACHE_VERSION на деплое

2.1 returnToken — JWT в теле ответа · low

apps/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 · low

apps/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; здесь этого
    нет
    . Под твою модель — хорошо.
  • Навигации: network-first с фолбэком на закэшенную оболочку; статика: stale-while-revalidate, кэшируются
    только 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. Чего тут можно НЕ бояться

  • Утечки приватных данных через PWA-кэш — нет: весь приватный трафик (/api) SW не кэширует (§2.4).
  • CSRF на мутациях — мутации идут JSON-POST'ами, требующими preflight; кука sameSite:lax отсекает
    произвольные кросс-сайты. Остаточный риск сведён к co-tenancy на самом localhost (§2.2), что для
    доверенной группы нерелевантно.

5. Охват и проверки

  • Прочитан полный дифф PR #116 (server: 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).
  • Spot-check подтверждено: returnToken опт-ин и токен отзываемый; CORS включает localhost + credentials;
    Swagger при включении без guard; SW не трогает /api//socket.io//collab и не кэширует приватные
    ответы; регистрация SW только в проде.
  • Вне scope (намеренно): подсистема ассистента и «кража токенов» — её PR #116 не меняет.
# Red-team отчёт — PR #116 (под реальную модель угроз) — 2026-06-21 > Цель: **только** PR #116 — `feat(mobile): bootstrap mobile app (PWA + Capacitor + backend auth/CORS)` > (<https://gitea.vvzvlad.xyz/vvzvlad/gitmost/pulls/116>, ветка `feature/mobile-app-bootstrap`). > Разбирается ровно дифф этого PR (14 файлов): `returnToken` в логине, явный CORS, опциональный Swagger, > PWA service worker + Capacitor-конфиг. Метод: read-only. ## 0. Модель угроз (с твоих слов) Локальный бложек, ~5 доверенных людей; снаружи целенаправленно не атакуют, DoS не рассматриваем. «Кража токенов» (чтобы аноним не жёг платные LLM-токены) — единственное, что по-настоящему волнует, но **это про подсистему ассистента, а её PR #116 не трогает** (см. §3). Поэтому здесь — оценка именно изменений #116 под эту линзу: не параноидально, по делу. ## 1. Вывод одной строкой В PR #116 **серьёзного под твою модель угроз нет.** Всё, что меняется — низкий риск. Есть 3 мелочи гигиены и одна приятная деталь (PWA-кэш приватных данных не сохраняет). Можно мержить; пункты ниже — по желанию. ## 2. Что меняет PR #116 и насколько это важно | # | Изменение | Файл | severity (под твою модель) | действие | |---|-----------|------|----------------------------|----------| | 1 | `returnToken`: JWT в теле ответа | `auth.controller.ts`, `login.dto.ts` | low | держать веб-клиент на куке | | 2 | CORS: хардкод `localhost` + `credentials:true` + `!origin` разрешён | `main.ts` | low | опц. убрать localhost в проде | | 3 | Swagger `/api/docs` без авторизации (если включить) | `main.ts`, `environment.service.ts` | low | `SWAGGER_ENABLED=false` в проде | | 4 | PWA service worker (рукописный) | `public/sw.js`, `main.tsx` | low (+плюс) | по желанию бампать `CACHE_VERSION` на деплое | ### 2.1 `returnToken` — JWT в теле ответа · low `apps/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` · low `apps/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; **здесь этого нет**. Под твою модель — хорошо. - Навигации: network-first с фолбэком на закэшенную оболочку; статика: stale-while-revalidate, кэшируются только 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. Чего тут можно НЕ бояться - **Утечки приватных данных через PWA-кэш** — нет: весь приватный трафик (`/api`) SW не кэширует (§2.4). - **CSRF на мутациях** — мутации идут JSON-POST'ами, требующими preflight; кука `sameSite:lax` отсекает произвольные кросс-сайты. Остаточный риск сведён к co-tenancy на самом `localhost` (§2.2), что для доверенной группы нерелевантно. ## 5. Охват и проверки - Прочитан полный дифф PR #116 (server: `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`). - Spot-check подтверждено: `returnToken` опт-ин и токен отзываемый; CORS включает localhost + `credentials`; Swagger при включении без guard; SW не трогает `/api`/`/socket.io`/`/collab` и не кэширует приватные ответы; регистрация SW только в проде. - Вне scope (намеренно): подсистема ассистента и «кража токенов» — её PR #116 не меняет.
vvzvlad added 1 commit 2026-06-21 18:25:02 +03:00
Close the two "[test coverage]" review gaps on PR #116 (mobile bootstrap):

- auth.controller.spec.ts: unit-test AuthController.login() returnToken
  branches via direct instantiation. returnToken:true returns exactly
  { authToken } alongside the httpOnly cookie; omitted/explicit-false return
  strictly undefined (the token must never leak into the response body for
  web clients) while the cookie is still set.
- environment.service.spec.ts: table-driven tests for getCorsAllowedOrigins()
  (split/trim/filter of CORS_ALLOWED_ORIGINS) and isSwaggerEnabled()
  (case-insensitive SWAGGER_ENABLED === 'true'), the two parsers feeding the
  CORS allowlist and Swagger exposure trust boundaries.

Tests only; no production code changed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
vvzvlad closed this pull request 2026-06-21 18:38:35 +03:00

Pull request closed

Sign in to join this conversation.