fix(mera/b2c): /estimate снова закрывался гвардом — короткий адрес завели в Caddy, но не в RouteGuard
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-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 55s

Замер на проде сразу после выкатки #2913: страница `/estimate` отдаёт 200 и
выглядит правильно, но после гидратации монтируется `GuardedRoute` —

  404 /trade-in/api/v1/me                             ×3
  404 /trade-in/api/v1/trade-in/support/anon/unread

плюс виджет чата поддержки из закрытого контура и экран «Не удалось проверить
доступ» внизу страницы. Форма при этом переставала работать ВОВСЕ: разметка с
сервера видна, поле принимает текст, а обработчики не подключены — человек
печатает адрес и не получает ни подсказок, ни ошибки. Проверено, что дело не в
API: ручной `fetch` из той же страницы отдаёт 200 и восемь подсказок.

Причина ровно та же, что чинил PR #2910 для `/`: список публичных путей в
гварде и матчер `@meraPages` в Caddyfile — одно множество, записанное в двух
местах. Тогда разошлись на `/`, теперь на `/estimate`. Оба раза страница
отдавала 200, ломалась только в браузере, и ни один тест этого не видел.

Комментарий в шапке гварда инцидент не предотвратил (он там был), поэтому
совпадение теперь запинено ТЕСТОМ: он читает Caddyfile и требует, чтобы каждый
путь из `@meraPages` проходил `isPublicPath`. Сверяемся с источником, а не
дублируем список третий раз.

Мутация — вернуть список к состоянию, которое уехало на прод:
  2 failed / 3 passed
  × пропускает короткие адреса публичного домена
  × совпадает с тем, что публичный домен реально раздаёт (Caddyfile)
откат → 5 passed.

Плюс vitest 56 passed, tsc, lint, isolation guard 41 файл.

`Caddyfile` уже в paths-фильтре фронтового лэйна (#2913), так что правка одного
лишь конфига теперь тоже гоняет этот гейт.
This commit is contained in:
bot-backend 2026-08-16 10:39:15 +03:00
parent 51a2d7db84
commit e74e9106b3
2 changed files with 54 additions and 5 deletions

View file

@ -65,12 +65,24 @@ const PUBLIC_PATHS = ["/mera-public"];
* проверить доступ». Замер на проде: три вызова `/me` плюс подтянутый следом
* клиент чата поддержки.
*
* Те же четыре коротких адреса раздаёт Caddy на публичном домене, и только на
* нём: на `gendsgn.ru` их ловит catch-all 404, а `/` там отдаёт server-side
* redirect на `/v2` из `next.config` то есть приложение по пути `/` вообще не
* рендерится и bypass ему не достаётся.
* Те же короткие адреса раздаёт Caddy на публичном домене, и только на нём: на
* `gendsgn.ru` их ловит catch-all 404, а `/` там отдаёт server-side redirect на
* `/v2` из `next.config` то есть приложение по пути `/` вообще не рендерится
* и bypass ему не достаётся.
*
* ЭТОТ СПИСОК ОБЯЗАН СОВПАДАТЬ С МАТЧЕРОМ `@meraPages` В КОРНЕВОМ Caddyfile.
* Повтор инцидента 16.08.2026: короткий `/estimate` завели в Caddy и забыли
* здесь. Страница отдавала 200 и выглядела правильно, но после гидратации
* монтировался `GuardedRoute` три запроса к `/me` (404, API на этом хосте не
* проксируется), виджет чата поддержки из закрытого контура и экран «Не удалось
* проверить доступ» внизу. Форма при этом переставала работать вовсе: разметка
* с сервера видна, а обработчики не подключались человек печатал адрес и не
* получал ни подсказок, ни ошибки.
*
* Совпадение теперь запинено тестом (`__tests__/RouteGuard.publicPaths.test.ts`
* читает Caddyfile), потому что комментарий этот инцидент уже не предотвратил.
*/
const PUBLIC_SHORT_PATHS = ["/", "/oferta", "/refund", "/privacy"];
const PUBLIC_SHORT_PATHS = ["/", "/estimate", "/oferta", "/refund", "/privacy", "/v3"];
export function isPublicPath(rawPath: string): boolean {
// Точное совпадение — ОТДЕЛЬНОЙ веткой и первым делом. Префиксную проверку

View file

@ -1,3 +1,6 @@
import { readFileSync } from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { isPublicPath } from "../RouteGuard";
@ -25,11 +28,45 @@ describe("isPublicPath", () => {
it("пропускает короткие адреса публичного домена", () => {
expect(isPublicPath("/")).toBe(true);
expect(isPublicPath("/estimate")).toBe(true);
expect(isPublicPath("/oferta")).toBe(true);
expect(isPublicPath("/refund")).toBe(true);
expect(isPublicPath("/privacy")).toBe(true);
});
/**
* ГЛАВНЫЙ ГЕЙТ, добавленный после повтора инцидента 16.08.2026.
*
* Список публичных путей в гварде и матчер `@meraPages` в Caddyfile это
* одно и то же множество, записанное в двух местах. Первый раз разошлись на
* `/`: домен появился, гвард о нём не знал. Второй раз на `/estimate`:
* короткий адрес завели в Caddy, в гвард добавить забыли. Оба раза страница
* отдавала 200 и выглядела правильно, а ломалась только после гидратации
* то есть ни один тест и ни один смоук по коду ответа этого не видел.
*
* Проверка читает конфиг прокси, а не дублирует список: единственный способ
* не разойтись сверяться с источником.
*/
it("совпадает с тем, что публичный домен реально раздаёт (Caddyfile)", () => {
// __tests__ → auth → components → src → frontend → tradein-mvp → корень.
const repoRoot = path.resolve(__dirname, "../../../../../..");
const caddyfile = readFileSync(path.join(repoRoot, "Caddyfile"), "utf8");
const served = caddyfile
.match(/@meraPages path ([^\n]+)/)?.[1]
.split(/\s+/)
.filter(Boolean);
expect(served, "не нашли матчер @meraPages — проверка стала бы пустой").toBeTruthy();
for (const route of served ?? []) {
expect(
isPublicPath(route),
`${route} раздаётся на meraocenka.ru, но гвард считает его закрытым — ` +
"после гидратации на нём смонтируется GuardedRoute и страница перестанет работать",
).toBe(true);
}
});
it("НЕ делает публичным закрытый контур — главный риск правки", () => {
for (const closed of [
"/v2",