feat(mera/b2c): публичный периметр на отдельном поддомене — этап 1 из 8 #2545

Open
lekss361 wants to merge 1 commit from feat/mera-b2c-perimeter into main
Owner

НЕ МЕРЖИТЬ до появления DNS-записи

Требуется A-запись mera.gendsgn.ru → IP VPS. Что будет без неё: конфиг синтаксически валиден, перезагрузка Caddy не падает, остальные домены не страдают (автоматический HTTPS изолирован по хостам). Но выпуск сертификата для поддомена провалится и будет ретраиться с задержкой, а две из четырёх смоук-проверок закономерно покраснеют.

Зачем этот этап

Первый из восьми в плане запуска B2C. Открывает периметр без функционала — публичной страницы-заглушки достаточно, чтобы доказать главное: B2B не приоткрылся. API не трогается вообще.

Порядок в плане жёсткий: периметр открывается последним по функционалу, но первым по инфраструктуре — чтобы всё остальное строилось за уже проверенной границей.

Почему отдельный поддомен, а не путь в текущем домене

Вся защита сегодня — это один блок route {}, где импорт basic_auth закрывает всё, что ниже по файлу, а публичность делается исключениями выше. Модель «запрещено всё, кроме дырок», порядко-зависимая — сам файл предупреждает об этом в шапке. Каждая новая дырка это шанс ошибиться матчером и открыть B2B-дашборд вместе с историей чужих оценок.

На отдельном site-блоке B2B-маршрутов физически нет: basic_auth не импортирован, разрешены ровно два маршрута, всё остальное — 404 через финальный catch-all. Открывать нечего.

Честное ограничение изоляции

Изоляция получилась на уровне маршрутизации, а не бандла. Next.js App Router не даёт независимый корневой layout без переноса всех существующих маршрутов в отдельную группу — это слишком крупный рефактор для первого этапа. Поэтому:

  • корневой layout по-прежнему оборачивает всё дерево, публичная страница исключена обходом в RouteGuard (тот же приём, что уже используется для превью, но безусловный — страница публична по продуктовому решению);
  • сама страница — серверный компонент, без "use client", без единого хука (useMe/useQuota/useHistory не импортируются), провайдер чата не подключён;
  • код B2B-компонентов лежит в том же бандле. Новой утечки это не создаёт: статика trade-in и так публична на основном домене, но и заявленной «физической изоляции» из плана здесь нет.

Полная изоляция бандла — отдельная задача, если решим, что она нужна.

Смоук периметра — главный артефакт этапа

Четыре проверки, остаются навсегда как регресс-тест:

  1. публичный поддомен отдаёт 200 анониму;
  2. B2B-путь на публичном поддомене отдаёт 404 — не 200 и не 401, то есть allowlist не пробит;
  3. gendsgn.ru/trade-in/v2 по-прежнему 401 анониму;
  4. gendsgn.ru/api/v1/admin/* по-прежнему 401 анониму.

Запускается вручную и ежедневно по расписанию, а не как пре-мерж гейт: проверки требуют живого DNS и выпущенного сертификата, чего в контексте PR быть не может.

Побочный эффект мержа

PR трогает и корневой Caddyfile, и фронтенд trade-in, поэтому сработают оба деплой-пайплайна. Оба идемпотентны, чекаут общий, порядок не важен.

Test plan

  • Скобки конфига сбалансированы, синтаксис сверен с прецедентами в том же файле
  • tsc --noEmit и next lint — чисто
  • Workflow валиден
  • Реальная проверка периметра — только после DNS и деплоя
  • caddy fmt локально не прогнать (Docker не запущен)
## ⛔ НЕ МЕРЖИТЬ до появления DNS-записи Требуется **A-запись `mera.gendsgn.ru` → IP VPS**. Что будет без неё: конфиг синтаксически валиден, перезагрузка Caddy не падает, остальные домены не страдают (автоматический HTTPS изолирован по хостам). Но выпуск сертификата для поддомена провалится и будет ретраиться с задержкой, а две из четырёх смоук-проверок закономерно покраснеют. ## Зачем этот этап Первый из восьми в плане запуска B2C. Открывает **периметр без функционала** — публичной страницы-заглушки достаточно, чтобы доказать главное: B2B не приоткрылся. API не трогается вообще. Порядок в плане жёсткий: периметр открывается последним по функционалу, но первым по инфраструктуре — чтобы всё остальное строилось за уже проверенной границей. ## Почему отдельный поддомен, а не путь в текущем домене Вся защита сегодня — это **один блок `route {}`**, где импорт basic_auth закрывает всё, что ниже по файлу, а публичность делается исключениями выше. Модель «запрещено всё, кроме дырок», порядко-зависимая — сам файл предупреждает об этом в шапке. Каждая новая дырка это шанс ошибиться матчером и открыть B2B-дашборд вместе с историей чужих оценок. На отдельном site-блоке B2B-маршрутов **физически нет**: `basic_auth` не импортирован, разрешены ровно два маршрута, всё остальное — `404` через финальный catch-all. Открывать нечего. ## Честное ограничение изоляции Изоляция получилась **на уровне маршрутизации, а не бандла**. Next.js App Router не даёт независимый корневой layout без переноса всех существующих маршрутов в отдельную группу — это слишком крупный рефактор для первого этапа. Поэтому: - корневой layout по-прежнему оборачивает всё дерево, публичная страница исключена обходом в `RouteGuard` (тот же приём, что уже используется для превью, но безусловный — страница публична по продуктовому решению); - сама страница — серверный компонент, без `"use client"`, без единого хука (`useMe`/`useQuota`/`useHistory` не импортируются), провайдер чата не подключён; - код B2B-компонентов лежит в том же бандле. Новой утечки это не создаёт: статика trade-in и так публична на основном домене, но и заявленной «физической изоляции» из плана здесь нет. Полная изоляция бандла — отдельная задача, если решим, что она нужна. ## Смоук периметра — главный артефакт этапа Четыре проверки, остаются навсегда как регресс-тест: 1. публичный поддомен отдаёт 200 анониму; 2. B2B-путь **на публичном поддомене** отдаёт 404 — не 200 и не 401, то есть allowlist не пробит; 3. `gendsgn.ru/trade-in/v2` по-прежнему 401 анониму; 4. `gendsgn.ru/api/v1/admin/*` по-прежнему 401 анониму. Запускается вручную и ежедневно по расписанию, а не как пре-мерж гейт: проверки требуют живого DNS и выпущенного сертификата, чего в контексте PR быть не может. ## Побочный эффект мержа PR трогает и корневой `Caddyfile`, и фронтенд trade-in, поэтому сработают оба деплой-пайплайна. Оба идемпотентны, чекаут общий, порядок не важен. ## Test plan - [x] Скобки конфига сбалансированы, синтаксис сверен с прецедентами в том же файле - [x] `tsc --noEmit` и `next lint` — чисто - [x] Workflow валиден - [ ] Реальная проверка периметра — только после DNS и деплоя - [ ] `caddy fmt` локально не прогнать (Docker не запущен)
lekss361 added 1 commit 2026-07-28 12:03:21 +00:00
feat(mera): открыть публичный B2C-периметр МЕРА (ЭТАП 1, без функционала)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 11s
CI / changes (pull_request) Successful in 12s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m17s
2b86e94c5b
Отдельный поддомен mera.gendsgn.ru вместо дырки в auth-gate gendsgn.ru — на
новом site-блоке B2B-маршруты (v2, admin, scrapers, /trade-in/api/*) физически
не объявлены, basic_auth не импортируется вообще. Allowlist-by-default: только
корень "/" (rewrite на /trade-in/mera-public) + статика Next _next/*, всё
остальное — 404.

Публичная страница-заглушка (tradein-mvp/frontend/src/app/mera-public/) не
тянет RouteGuard/SupportChatProvider функционально — RouteGuard получил
безусловный bypass для этого пути (аналог существующего /ui-preview,
но always-on, не за dev-флагом); сама страница — server component без
useMe/useQuota/useHistory. robots: noindex на этом этапе.

CI-регресс на периметр: scripts/smoke-mera-perimeter.sh (4 проверки: публичный
поддомен 200, B2B-путь на нём 404, gendsgn.ru/trade-in/v2 и /api/v1/admin/*
по-прежнему 401 анониму) + .forgejo/workflows/perimeter-smoke.yml
(workflow_dispatch + daily cron — не pre-merge гейт, требует реального DNS).

Требуется DNS: A-record mera.gendsgn.ru → IP VPS. До появления записи ACME
для нового хоста будет ретраить с backoff, остальные site-блоки не страдают
(автоматический HTTPS в Caddy изолирован per-hostname).
All checks were successful
CI Trade-In / changes (pull_request) Successful in 11s
CI / changes (pull_request) Successful in 12s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m17s
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/mera-b2c-perimeter:feat/mera-b2c-perimeter
git checkout feat/mera-b2c-perimeter

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git checkout main
git merge --no-ff feat/mera-b2c-perimeter
git checkout feat/mera-b2c-perimeter
git rebase main
git checkout main
git merge --ff-only feat/mera-b2c-perimeter
git checkout feat/mera-b2c-perimeter
git rebase main
git checkout main
git merge --no-ff feat/mera-b2c-perimeter
git checkout main
git merge --squash feat/mera-b2c-perimeter
git checkout main
git merge --ff-only feat/mera-b2c-perimeter
git checkout main
git merge feat/mera-b2c-perimeter
git push origin main
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2545
No description provided.