From 9810d47e42d9275cf2520556f517d411058ddcb3 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Tue, 15 Sep 2026 05:42:16 +0300 Subject: [PATCH 1/6] docs: refine backend conventions and routing --- .ai/CONVENTIONS.md | 30 ++------ .ai/SOURCES.md | 2 +- .ai/architecture.md | 44 +++++++++++ .ai/conventions/backend-architecture.md | 93 +++++------------------ .ai/conventions/backend-data.md | 7 ++ .ai/conventions/backend-error-handling.md | 40 ++++++++++ .ai/conventions/backend-http-api.md | 17 ++++- .ai/conventions/backend-integrations.md | 7 +- .ai/conventions/backend-observability.md | 24 ++++++ .ai/conventions/backend-testing.md | 8 +- .ai/conventions/security-auth.md | 9 +++ AGENTS.md | 51 ++++++------- 12 files changed, 201 insertions(+), 131 deletions(-) create mode 100644 .ai/architecture.md create mode 100644 .ai/conventions/backend-error-handling.md create mode 100644 .ai/conventions/backend-observability.md diff --git a/.ai/CONVENTIONS.md b/.ai/CONVENTIONS.md index 73d81bd..5791928 100644 --- a/.ai/CONVENTIONS.md +++ b/.ai/CONVENTIONS.md @@ -1,25 +1,9 @@ -# Backend router +# Backend conventions: вход -Объединяй triggers из смысла задачи, путей и обнаруженных boundaries. -Читай выбранные guides до решения; якорь допускает чтение только нужного раздела. -Просто backend-файл не требует всех conventions. +Актуальный routing по задаче, пути и boundary находится в [AGENTS](../AGENTS.md). +Выбирай только активированные темы; при новой границе повтори routing. +[Architecture](architecture.md) объясняет mental model, [SOURCES](SOURCES.md) +ведёт к системным фактам, specifications и ADR по конкретному вопросу. -| Решение / сигнал | Читать | -| --- | --- | -| Новый файл/suffix/layer; поиск feature owner; progressive structure; перенос responsibility | [Architecture](conventions/backend-architecture.md) | -| TS vocabulary, domain literals, magic values | [Domain literals](conventions/backend-architecture.md#domain-literals-and-magic-values) | -| Controller, DTO, route/status, validation/exception, публичная HTTP-семантика | [HTTP API](conventions/backend-http-api.md) | -| Repository/data access; Prisma/schema/query, transaction, migration, atomic write | [Data](conventions/backend-data.md) | -| Тесты или behavior с testing risk | [Testing](conventions/backend-testing.md) | -| JWT, cookie, login/refresh/logout, guard/access, token encryption, sensitive logs | [Security](conventions/security-auth.md) | -| External client/integration boundary; T-Bank/gRPC/proto, retry/rate limit/cache | [Integrations](conventions/backend-integrations.md) | -| Logging/request context без изменения secrets/auth | [Logging](conventions/backend-architecture.md#logging) | -| Новый abstraction/dependency, refactor, неясный scope | [Общий выбор реализации](../../AGENTS.md) | -| Изменение request/response/route/status/error shape или Swagger metadata | HTTP API + [Источник HTTP-контракта](../../docs/architecture/http-api.md#источник-контракта) | - -Новый endpoint обычно требует HTTP API + Testing; Architecture/Data/Security -добавляются по новым ответственности, persistence или access rules. -Изменение одного Prisma query без HTTP/auth границы не требует HTTP API/Security. -Auth refresh активирует Security + Data + Testing; HTTP API добавляется, если -меняется controller/HTTP contract. Exact docs/spec sources выбирай через SOURCES. -Повтори routing при расширении diff, включая новые и unstaged файлы. +Этот адрес сохранён для существующих входящих ссылок; второй копии правил +и таблицы маршрутов здесь нет. diff --git a/.ai/SOURCES.md b/.ai/SOURCES.md index 8a78838..07082e0 100644 --- a/.ai/SOURCES.md +++ b/.ai/SOURCES.md @@ -2,7 +2,7 @@ | Что нужно установить | Владелец | | --- | --- | -| Implementation decision и безопасный pattern | [CONVENTIONS](CONVENTIONS.md) | +| Implementation decision и безопасный pattern | [AGENTS](../AGENTS.md) | | HTTP errors, DTO semantics, public wire shape | [docs: HTTP API](../../docs/architecture/http-api.md); runtime OpenAPI из Nest — единственный editable source | | Auth/session, account access, token encryption | [Аутентификация](../../docs/domains/authentication/overview.md), [доступ к данным](../../docs/architecture/containers.md#хранимые-данные-и-границы-доступа), [подключение брокера](../../docs/integrations/tbank.md) | | Prisma schema/data ownership и migration context | [docs: Data](../../docs/architecture/containers.md#хранимые-данные-и-границы-доступа) | diff --git a/.ai/architecture.md b/.ai/architecture.md new file mode 100644 index 0000000..48724cd --- /dev/null +++ b/.ai/architecture.md @@ -0,0 +1,44 @@ +# Backend mental model + +NestJS + TypeScript modular monolith. Composition root `AppModule` подключает +бизнес-модули, validated configuration, Prisma и Pino. Общая `configureApp` +задаёт `/api/v1`, CORS/cookies, validation, request logging и error filter. +Точные versions/scripts — в [package.json](../package.json); source code +определяет фактическую реализацию. + +```text +HTTP controller → application service/use case → pure domain rules + ↓ + repository/data access → Prisma → SQLite + ↓ + integration client → T-Bank gRPC / MOEX / NCR HTTP +``` + +`modules/auth`, `modules/banking/bank-cards` и области `modules/investments` +владеют пользовательскими возможностями. Сложные модули разделены на +presentation/application/domain/infrastructure; компактные остаются плоскими. +`*.module.ts` — Nest DI, `*.api.ts` — используемые cross-module exports. +Application использует конкретные providers; обязательных ports на каждый +класс нет. [Module convention](conventions/backend-architecture.md). + +Stocks/bonds — slices одного instruments module: catalog владеет identity/cache, +market — котировками, income — выплатами/личными позициями. Общая orchestration +в `instruments/application` использует DTO/provider records и не становится pure +domain из-за extraction в функцию. Boundary mappers преобразуют внешние records; +чистые расчёты portfolio/banking остаются у domain owner. + +PrismaService владеет DB lifecycle. Самостоятельные use cases получают данные +через concrete repositories; существующие связные CRUD/transaction services +сохраняют прямые queries по [data convention](conventions/backend-data.md). +Money — currency + decimal string; расчёты используют точные общие helpers. + +Auth управляет identity/session; TbankAccessService проверяет доступ к счёту и +connection generation. Integration clients владеют protocol/deadline/retry; +provider records преобразуются перед чистыми domain calculations. +Пользовательский `tbank-connection` не владеет низкоуровневым gRPC client. + +Runtime OpenAPI из Nest → предоставленный frontend snapshot → generated types. +Problem Details фильтруются общей HTTP boundary; StructuredLogger добавляет +request context и ограничивает поля событий. Jest unit tests, изолированные +SQLite/HTTP suites и OpenAPI parity доказывают разные части поведения. +Системные потоки и ADR выбираются через [SOURCES](SOURCES.md). diff --git a/.ai/conventions/backend-architecture.md b/.ai/conventions/backend-architecture.md index a3e16f1..10734e8 100644 --- a/.ai/conventions/backend-architecture.md +++ b/.ai/conventions/backend-architecture.md @@ -2,11 +2,8 @@ ## Module-first organization -Backend — domain-oriented modular monolith с лёгким разделением -по ответственности. `src/modules` группирует код по бизнес-возможности: `auth`, `banking/bank-cards`, области `investments`. -В инвестициях отдельные Nest modules владеют `brokerage-accounts`, `portfolio`, -`operations`, `income`, `upcoming-events`, `instruments`, `tbank-connection`. -`instruments/stocks` и `instruments/bonds` — slices внутри одного модуля. +Устройство и data flow — в [mental model](../architecture.md). +Сначала выбери business owner в `modules/`, затем ответственность внутри него. ### Progressive structure @@ -36,15 +33,6 @@ feature/ Для одного controller или пары DTO дополнительная поддиректория не нужна. Пустые папки, новые barrels, CQRS и interfaces не требуются для этой организации. -В текущем коде слои используют `auth`, `bank-cards`, `portfolio`, `operations` -и slices `instruments/stocks`, `instruments/bonds`. Общие application helpers -инструментов находятся в `instruments/application`; `instruments.module.ts` -компонует оба slice. `stock-position.ts` и `instrument-position.ts` формируют -результат из portfolio DTO; `instrument-market.ts` обрабатывает внешние данные -и ошибки Nest. Это application code, а не независимая domain model. -`brokerage-accounts`, `income`, `upcoming-events`, `tbank-connection` остаются -компактными плоскими modules. Наличие tests само по себе не требует слоёв. - ## Поиск и создание файла При работе над feature проходи владельцев в порядке: @@ -134,6 +122,10 @@ Tests могут импортировать внутренний owner для п Новый export требует текущего потребителя; controller и persistence остаются приватными. Не добавляй `index.ts` на каждом уровне. +Circular dependencies устраняй изменением ownership/composition. `forwardRef()` +не является стандартным исправлением boundaries; исключение требует конкретной +неизбежной связи и проверки DI initialization. + Каждый provider регистрируется у одного владельца. Потребители импортируют его module, а не повторяют provider: это сохраняет единственный кэш и лимитер. `AuthModule` экспортирует guard; наличие JWT не заменяет account access check. @@ -169,30 +161,18 @@ DTO остаются транспортными runtime classes; существ Сохраняй существующий error contract: HTTP exceptions остаются на application/ transport boundary. Чистому расчёту не нужен новый framework ошибок. +Mapping и stable codes — по [Errors](backend-error-handling.md). Точные ограничения — [HTTP](backend-http-api.md), [Data](backend-data.md), [Security](security-auth.md), [Integrations](backend-integrations.md). ## Сценарии и размер сервисов -Разделяй независимые операции, а не строки. `BankCardsService` отвечает за карты, -`BankOperationsService` — журнал/переводы/возвраты, `ExpenseCategoriesService` — -категории, `GetBankSummaryUseCase` — сводку. `BankCardsRepository` сохраняет -общий transaction retry и owner lookup; денежные транзакции не разрываются. -Чистые расчёты банковских карт находятся в `domain` этого модуля. +Разделяй независимые операции, а не строки. Пример: `BankCardsService` владеет +картами, `BankOperationsService` — журналом/переводами, `GetBankSummaryUseCase` — +сводкой через repository и чистый расчёт. Не разрывай денежную transaction, +общий cache/limiter или pending promises при extraction. -У инструментов `StockCatalogService` / `BondCatalogService` хранят идентичность -листинга и общий кэш slice; market service — котировки/свечи, income service — выплаты/личные позиции. -`GetStockCompanyUseCase` владеет сведениями об эмитенте. Ключи, TTL и pending -promises разделяют прежний экземпляр кэша; extract не должен менять generation. -Portfolio и operations сохраняют связную координацию снимков/страниц и кэшей. -Расчёты портфеля находятся в `portfolio/domain`; преобразование брокерских -позиций — в `tbank-portfolio.mapper.ts`. `bond-money.mapper.ts` преобразует -ответ клиента и не является value object. - -Выбирай обычную функцию для чистого вычисления; repository contract — только -для реальной границы. CQRS/buses/events, BaseRepository/BaseService, DI token -для каждого класса, mapper/presenter для каждого DTO и новые dependencies -не нужны для структурного переноса. +Расширение abstractions — по [change scope](../../../.ai/conventions/code-health.md#change-scope). ## Common @@ -205,39 +185,12 @@ protobuf units/nano conversion — T-Bank integration. Domain используе ## Domain literals and magic values -Finite business vocabulary must be modeled explicitly; incidental implementation -literals stay inline. Это относится к error codes, statuses, operation/account/ -instrument/transaction types, permissions, roles, feature states и бизнес-режимам. - -Перед новым определением найди existing owner/type/value. В branching и comparisons -используй символическое значение владельца; не повторяй raw domain literals. -Храни словарь у domain/module/entity с соблюдением границ слоёв, без глобальной -свалки `constants.ts`. Сохраняй принятый pattern; для нового project-owned словаря -без подходящего аналога используй `as const` с derived union: - -```ts -const BankErrorCode = { - InvalidDate: "BANK_INVALID_DATE", - ImmutableOperation: "BANK_IMMUTABLE_OPERATION", -} as const; -type BankErrorCode = (typeof BankErrorCode)[keyof typeof BankErrorCode]; - -// Вместо code === "BANK_INVALID_DATE": -if (code === BankErrorCode.InvalidDate) { /* existing handling */ } -``` - -Значимые numeric policies (timeout, retry, page size, threshold, coefficient) -называй по смыслу и единицам: `REQUEST_TIMEOUT_MS`, `CACHE_TTL_SECONDS`, -`MAX_RETRY_COUNT`, `PAGE_SIZE`. Оставляй inline UI labels в принятом месте, -log messages, индексы, `index + 1`, `Math.max(value, 0)`, `page ?? 1`, -`percentage * 100` и очевидные локальные детали без semantic benefit. -При неясном owner/смысле оставь значение и укажи ограничение в review. - -Не мигрируй корректные existing enum/union styles массово. Не добавляй ради -правила factories, registries, schemas, loaders, abstraction layers, dependencies, -custom lint/AST rules, generation/validation scripts или тесты существования -констант. Semantic review проверяет ownership, единое написание и неизменность -wire values; обычные behavior tests проверяют поведение. +Общий выбор symbolic values и числовых policies — +[Domain values](../../../.ai/conventions/code-health.md#domain-values). +Сохраняй strict TS и explicit boundary types. Для недоверенного input выбирай +`unknown` + runtime narrowing; отключённый lint `no-explicit-any` не делает +`any` нормой domain/application model. `declare` допустим для заполняемых извне +DTO fields, не для сокрытия неинициализированного owned state. OpenAPI enum metadata и runtime validation должны читать тот же vocabulary, что application/domain. Например, `BankOperationKind` принадлежит bank-cards/domain, @@ -255,14 +208,8 @@ semantic magic strings проверяются conventions/review без ново ## Logging -Единственный logger — nestjs-pino; application events используют существующий -[StructuredLogger](../../src/common/logging/structured-logger.service.ts) и -request context. Выбирай стабильное имя события и разрешённые scalar fields. -Wrapper отбрасывает неизвестные поля, но это не разрешение помещать secret -в `message`/`stack`: review sensitive logging обязателен. -Не добавляй второй logger или сырой request/body/error payload. -Источник rationale — [ADR 008](../../../docs/adr/008-pino-backend-logging.md); -при secrets/user data читай также Security. +События, levels, correlation и safe fields — по +[Observability](backend-observability.md). ## Проверка diff --git a/.ai/conventions/backend-data.md b/.ai/conventions/backend-data.md index 04954b5..bd6cf5b 100644 --- a/.ai/conventions/backend-data.md +++ b/.ai/conventions/backend-data.md @@ -22,6 +22,13 @@ interface/port/adapter. Он владеет queries, includes/filtering, DB mapp согласованным чтением. Суффиксы `*.persistence.ts`, `*.store.ts`, `*.dao.ts` для той же роли не используем. Nest module регистрирует repository приватно. +Новые business decisions (можно ли менять архивную карту, допустим ли перевод) +принадлежат application/domain; repository сохраняет persistence и ownership +ограничения. Existing checks/HTTP helpers в BankCardsRepository — migration +candidate, не повод дублировать их при новом use case или разрывать transaction. +`BaseRepository`, `GenericRepository` и RepositoryFactory поверх Prisma +не нужны без отдельной общей ответственности. + `BankCardsRepository.summaryData` проверяет владельца карты и получает операции с категориями в одной read transaction. `GetBankSummaryUseCase` выбирает период, вызывает repository, затем чистый `bankSummary` и формирует ответ. Prisma records diff --git a/.ai/conventions/backend-error-handling.md b/.ai/conventions/backend-error-handling.md new file mode 100644 index 0000000..7072d38 --- /dev/null +++ b/.ai/conventions/backend-error-handling.md @@ -0,0 +1,40 @@ +# Backend errors + +## Поток + +```text +application/domain outcome → application mapping → HttpExceptionFilter + → application/problem+json +``` + +Сохраняй принятый профиль RFC 9457: `status`, стабильный публичный `code`, +для validation — `errors: [{ pointer, detail }]`. Точный контракт и известное +расхождение validation описаны в [HTTP API](../../../docs/architecture/http-api.md). +Не добавляй `type/title/instance` как обязательные поля без contract change. + +Чистый domain не импортирует Nest HTTP exceptions. Существующий application +service может бросить подходящий `HttpException` с public code; не нужен новый +error framework/registry. Если domain outcome требует HTTP interpretation, +маппинг принадлежит application/transport boundary. + +## Codes и безопасность + +- Используй existing code владельца, не сравнивай внутренние сообщения. + Новое значение имеет один symbolic owner; metadata/validation и consumers + согласуются по [domain values](../../../.ai/conventions/code-health.md#domain-values). +- Общий envelope, default status mapping и unexpected errors принадлежат + [HttpExceptionFilter](../../src/common/filters/http-exception.filter.ts). + Не дублируй `res.status().json()` в controllers/services. +- Неожиданный exception → `500 / INTERNAL_SERVER_ERROR`, без message/stack, + SQL, credentials или raw upstream payload в ответе. +- External auth failure и временная недоступность имеют разные outcomes; + translation выполняется на existing integration/access boundary. +- Логи используют [observability](backend-observability.md). Redaction не + делает произвольное сообщение ошибки безопасным. + +## Проверка + +Для изменённого кода проверь observable status/code и сохранность данных. +Для validation нужен request через `configureApp`: fabricated `ValidationError` +в unit test filter не доказывает форму, выходящую из реального ValidationPipe. +OpenAPI parity проверяет schema, а не response pipeline; [Testing](backend-testing.md). diff --git a/.ai/conventions/backend-http-api.md b/.ai/conventions/backend-http-api.md index 850edcd..fcb1022 100644 --- a/.ai/conventions/backend-http-api.md +++ b/.ai/conventions/backend-http-api.md @@ -35,10 +35,9 @@ ValidationPipe уже настроен в `configureApp` (`transform`, `whitelis описывает те же required/optional/nullable/enum/range значения. Используй типизированные response DTO, `ApiOkResponse`/другой фактический status -и существующий `ApiProblemResponses`. При новых ошибках задай публичный stable -`code`; сообщение внутреннего exception не является UI contract. Не возвращай -raw upstream error, Prisma record с secrets или stack. Текущая форма ошибок -описана у HTTP owner; не добавляй поля RFC только потому, что они встречаются в tutorial. +и существующий `ApiProblemResponses`. Prisma model не является public contract: +response выбирает поля явно. Stable codes, RFC 9457 профиль и mapping принадлежат +[Errors](backend-error-handling.md). **Безопасный путь:** используй exception и общий filter, а не локальный `res.status(...).json(...)`. Исключение — транспортная операция вроде cookie; @@ -46,6 +45,16 @@ raw upstream error, Prisma record с secrets или stack. Текущая фор ## Пример и проверка +Prefix `/api/v1` задаёт `configureApp`. Route/status/request/response/OpenAPI +change — изменение публичного backend contract; несовместимое изменение требует +согласованного перехода consumers, а не незаметной правки DTO. Новый API version +не вводи для внутреннего refactor. + +Pagination сохраняет contract endpoint: operations использует opaque cursor, +limit и DTO bounds. Не заменяй cursor offset/page и не добавляй global pagination +helper ради первого endpoint. Новая pagination явно определяет порядок, +границу страницы и end-of-list; reused cursor не должен менять filters/owner. + [Operations controller](../../src/modules/investments/operations/presentation/brokerage-operations.controller.ts) показывает guard, user.sub, DTO query и service delegation. Для нового field обновляй canonical source и generated artifacts в определённом diff --git a/.ai/conventions/backend-integrations.md b/.ai/conventions/backend-integrations.md index 1c5d3fb..9ade729 100644 --- a/.ai/conventions/backend-integrations.md +++ b/.ai/conventions/backend-integrations.md @@ -2,7 +2,8 @@ ## Выбор boundary -T-Bank gRPC принадлежит TbankClient; MOEX metadata — MoexClient. Сервисы use case +T-Bank gRPC принадлежит TbankClient; MOEX metadata — MoexClient; NCR ratings — +NcrRatingsClient. Сервисы use case используют объявленные клиентом records; SDK clients, metadata и transport errors остаются внутри integration. Frontend не вызывает T-Bank и не получает credentials/proto client. Reference source — @@ -40,6 +41,10 @@ Existing client уже владеет bounded FIFO queue, rate limit, deadline CA mechanism. Точные параметры и ограничения принадлежат [connection configuration](../../../docs/integrations/tbank.md#конфигурация-и-ограничения). +Эта queue/retry policy относится к T-Bank. MOEX/NCR HTTP clients используют +собственный bounded timeout и безопасный unavailable outcome; не приписывай им +gRPC retry policy. При изменении upstream сначала прочитай конкретный client. + При query/cache change установи key, user/account/generation ownership, freshness и поведение late response. Старый результат не может обслужить новую connection generation. Client Cached Snapshot frontend отличается от backend diff --git a/.ai/conventions/backend-observability.md b/.ai/conventions/backend-observability.md new file mode 100644 index 0000000..b3ff97c --- /dev/null +++ b/.ai/conventions/backend-observability.md @@ -0,0 +1,24 @@ +# Observability + +- Единственный logger — nestjs-pino. Application events используют + [StructuredLogger](../../src/common/logging/structured-logger.service.ts), + стабильное имя события и разрешённые scalar fields. `console.log` и второй + logger в production не используются. +- `info` — значимый lifecycle/outcome; `debug` — подробности успешного RPC; + `warn` — восстановимая проблема/retry; `error` — окончательный сбой. + Не добавляй log на каждый вход/выход функции или дубликат HTTP completion. +- RequestContextService и request middleware владеют correlation: серверный + request ID возвращается как `X-Request-ID`, входящий client ID хранится отдельно. + Передавай существующий context, не создавай собственный UUID в каждом service. +- Wrapper ограничивает поля, Pino redacts request secrets. Не логируй passwords, + tokens, cookie, Authorization, credentials, connection generation, raw body + или upstream error. Secret в `message/stack` не становится безопасным из-за + allowlist; правила [Security](security-auth.md) обязательны для sensitive data. +- Для ошибки используй public code и безопасный context операции/попыток; + [error mapping](backend-error-handling.md) не отдаёт внутренние детали клиенту. + +Причины — [ADR 008](../../../docs/adr/008-pino-backend-logging.md), системное +поведение — [HTTP observability](../../../docs/architecture/http-api.md#наблюдаемость). +Проверяй новые поля/redaction/correlation existing logging tests; не добавляй +тест на каждую строку log. Existing ESLint не запрещает все вызовы `console` +отдельным правилом: до настройки встроенного `no-console` это semantic review. diff --git a/.ai/conventions/backend-testing.md b/.ai/conventions/backend-testing.md index b86e45c..0b64f93 100644 --- a/.ai/conventions/backend-testing.md +++ b/.ai/conventions/backend-testing.md @@ -1,5 +1,9 @@ # Backend testing +Перед нетривиальными тестами сформулируй +[общий Test Intent](../../../.ai/conventions/testing.md); отдельный документ +не требуется. Trivial low-risk изменения обходятся без него. + ## Выбор уровня Проверяй риски доступа, денежных расчётов, целостности данных, конкурентных @@ -40,8 +44,8 @@ Unit specs colocated в `src/**/*.spec.ts`; HTTP/integration — `test/`. `npm run test:e2e` запускает все HTTP suites через изолированный SQLite runner; `npm run test:sqlite` — только auth/cards. Каждая suite получает новую временную базу со штатными migrations. Прямой Jest без runner отклоняется до DB-запросов. -`prisma.config.ts` исключает влияние унаследованного `RUST_LOG=warn/error`, -вызывающего ошибку fresh migration на Prisma 7.9.1. Подробности и регрессия запуска — +`prisma.config.ts` исключает влияние унаследованного `RUST_LOG=warn/error` +на fresh migration; точная Prisma version принадлежит manifest/lockfile. Подробности и регрессия запуска — [Backend README](../../README.md#реальная-sqlite-и-сохранность-данных). HTTP suites требуют разрешения открывать локальные порты. Ошибка sandbox не считается успешным тестом; повторите проверку в подходящем окружении. diff --git a/.ai/conventions/security-auth.md b/.ai/conventions/security-auth.md index 7534d4b..eafe2a9 100644 --- a/.ai/conventions/security-auth.md +++ b/.ai/conventions/security-auth.md @@ -24,6 +24,10 @@ frontend session/cache очищается и при network failure. ## Trust boundaries +Frontend не является security boundary. DTO runtime validation выполняется +server-side; whitelist не заменяет access checks. Environment/secret inputs +проверяет existing config owner; реальные credentials не попадают в fixtures. + Verified JWT не доказывает доступ к accountId из URL. Используй user.sub и existing account access policy; не доверяй userId/token из клиента как authority. Cross-user, invalid/expired/superseded token и concurrent requests — обязательные @@ -39,6 +43,11 @@ cache invalidation при замене/deletion и ограничение key ro обрабатывает существующий bcrypt flow. Не копируй требования о нормализации или длине из tutorial: сверяй runtime DTO, tests и feature source. +Предпочитай Prisma query API. Raw SQL допускается только по конкретной нужде +с параметризованными значениями; не конкатенируй untrusted input и не передавай +его в `queryRawUnsafe`/`executeRawUnsafe`. Dynamic identifiers выбирай из +закрытого server-owned набора. Проверка user ownership сохраняется и в SQL. + ## Безопасный путь и проверка При неясной ownership/crypto/session семантике прочитай exact source до edits. diff --git a/AGENTS.md b/AGENTS.md index 73f4afb..e35f322 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,30 +1,27 @@ -# Backend +# Backend router -NestJS API; общие правила работы — в root `AGENTS.md`. -До backend-решения прочитай [router](.ai/CONVENTIONS.md) и -[sources](.ai/SOURCES.md), затем только применимые документы/разделы. +Общие инструкции — [workspace AGENTS](../AGENTS.md). Объединяй triggers задачи, +путей и обнаруженных boundaries; читай только нужные темы до решения. +[Sources](.ai/SOURCES.md) выбирает факты/spec/ADR по возникшему вопросу. -## Проверка +| Решение / сигнал | Читать | +| --- | --- | +| Первое знакомство, data flow, общая архитектура | [Mental model](.ai/architecture.md) | +| Module/layer/public API/DI/suffix, owner, перенос responsibility | [Modules/application](.ai/conventions/backend-architecture.md) | +| TS vocabulary, domain literals, magic values | [Types/values](.ai/conventions/backend-architecture.md#domain-literals-and-magic-values) | +| Controller/DTO/validation, route/status/response, OpenAPI | [HTTP API](.ai/conventions/backend-http-api.md) | +| Application/domain/HTTP error, exception, code | [Errors](.ai/conventions/backend-error-handling.md) | +| Repository/Prisma/query, transaction, migration, atomic write | [Data](.ai/conventions/backend-data.md) | +| Tests или behavior с regression risk | [Testing](.ai/conventions/backend-testing.md) | +| JWT/cookie/session/guard/access, encryption, secrets | [Security](.ai/conventions/security-auth.md) | +| External client/gateway/SDK/proto, retry/rate limit/cache | [Integrations](.ai/conventions/backend-integrations.md) | +| Logging/context/levels, sensitive logs | [Observability](.ai/conventions/backend-observability.md); sensitive data также Security | +| Изменение публичного wire contract / Swagger metadata | HTTP API + [HTTP contract source](../docs/architecture/http-api.md#источник-контракта) | +| Helper/dependency/новый pattern, refactor/scope | [Code health](../.ai/conventions/code-health.md), для пакета — [Dependencies](../.ai/conventions/dependencies.md) | +| Выбор gate, tooling, завершение | [Guardrails](../.ai/conventions/guardrails.md) | -Перед Node-командами: `nvm use --silent` по `.nvmrc`. -После lockfile install `npm ci` полный gate — `npm run verify`: -zero-warning ESLint, application/test TypeScript, Jest и build. -`lint:fix`/`format` пишут файлы и запускаются отдельно от доказательства проверки. -`test:cov` — диагностика без общего threshold; scripts принадлежат `package.json`. - -HTTP contract changes сохраняют [направление контракта](../docs/architecture/http-api.md#источник-контракта) -и проходят contract check. Общий e2e запускается на отдельных временных DB; -применимость и маршрут проверки -описаны в [backend testing](.ai/conventions/backend-testing.md). -Generated Prisma client не редактируется вручную. Credentials, авторизация -доступа к счетам и внешние интеграции остаются server-side. - -ESLint/TypeScript не доказывают правильность domain boundaries, security или -денежных вычислений: для них нужны semantic review и risk-based tests. - -## Ветки и релизы - -Работа — в отдельной ветке от `develop`, затем PR в `develop`; в `main` напрямую -не пушить. Release/hotfix и синхронизация веток — по [root README](../README.md#релизы-и-ветки). -Commits и заголовки PR — Conventional Commits; для спецификаций допустим `spec:`. -Перед подготовкой релиза прочитай root README; версии вручную не повышать. +Новый endpoint обычно требует HTTP API + Testing; Data/Security добавляются +по persistence/access trigger. Auth refresh активирует Security + Data + Testing. +Повтори routing по итоговому diff, включая новые файлы. Migration backlog +читается только при соответствующем finding через +[plan](../.ai/migrations/conventions-v1/plan.md). -- 2.54.0 From 016782657acfd35ff3fd755462007938fac35156 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Tue, 15 Sep 2026 05:58:33 +0300 Subject: [PATCH 2/6] docs: simplify conventions layout --- .ai/CONVENTIONS.md | 9 - .ai/SOURCES.md | 19 -- .ai/architecture.md | 226 +++++++++++++++++- .ai/conventions/backend-architecture.md | 218 ----------------- .ai/conventions/{backend-data.md => data.md} | 4 +- ...nd-error-handling.md => error-handling.md} | 4 +- .../{backend-http-api.md => http-api.md} | 6 +- ...ackend-integrations.md => integrations.md} | 6 +- ...kend-observability.md => observability.md} | 2 +- .ai/conventions/security-auth.md | 2 +- .../{backend-testing.md => testing.md} | 0 AGENTS.md | 35 ++- README.md | 8 +- 13 files changed, 263 insertions(+), 276 deletions(-) delete mode 100644 .ai/CONVENTIONS.md delete mode 100644 .ai/SOURCES.md delete mode 100644 .ai/conventions/backend-architecture.md rename .ai/conventions/{backend-data.md => data.md} (98%) rename .ai/conventions/{backend-error-handling.md => error-handling.md} (95%) rename .ai/conventions/{backend-http-api.md => http-api.md} (95%) rename .ai/conventions/{backend-integrations.md => integrations.md} (95%) rename .ai/conventions/{backend-observability.md => observability.md} (94%) rename .ai/conventions/{backend-testing.md => testing.md} (100%) diff --git a/.ai/CONVENTIONS.md b/.ai/CONVENTIONS.md deleted file mode 100644 index 5791928..0000000 --- a/.ai/CONVENTIONS.md +++ /dev/null @@ -1,9 +0,0 @@ -# Backend conventions: вход - -Актуальный routing по задаче, пути и boundary находится в [AGENTS](../AGENTS.md). -Выбирай только активированные темы; при новой границе повтори routing. -[Architecture](architecture.md) объясняет mental model, [SOURCES](SOURCES.md) -ведёт к системным фактам, specifications и ADR по конкретному вопросу. - -Этот адрес сохранён для существующих входящих ссылок; второй копии правил -и таблицы маршрутов здесь нет. diff --git a/.ai/SOURCES.md b/.ai/SOURCES.md deleted file mode 100644 index 07082e0..0000000 --- a/.ai/SOURCES.md +++ /dev/null @@ -1,19 +0,0 @@ -# Источники backend - -| Что нужно установить | Владелец | -| --- | --- | -| Implementation decision и безопасный pattern | [AGENTS](../AGENTS.md) | -| HTTP errors, DTO semantics, public wire shape | [docs: HTTP API](../../docs/architecture/http-api.md); runtime OpenAPI из Nest — единственный editable source | -| Auth/session, account access, token encryption | [Аутентификация](../../docs/domains/authentication/overview.md), [доступ к данным](../../docs/architecture/containers.md#хранимые-данные-и-границы-доступа), [подключение брокера](../../docs/integrations/tbank.md) | -| Prisma schema/data ownership и migration context | [docs: Data](../../docs/architecture/containers.md#хранимые-данные-и-границы-доступа) | -| T-Bank/gRPC/cache, logging | [T-Bank](../../docs/integrations/tbank.md), [свежесть данных](../../docs/architecture/cross-cutting/data-freshness.md), [наблюдаемость](../../docs/architecture/http-api.md#наблюдаемость) | -| Money, account/position/income/cache meanings | [docs: Domain](../../docs/glossary.md) | -| Поведение конкретной feature | [docs: Features](../../docs/README.md#предметные-области) | -| Существующие behavioral requirements при необходимости | [specs index](../../specs/AI_INDEX.md) | -| Versions/scripts и scope автоматических checks | package.json/lockfile, ESLint/TypeScript/Jest/Prisma configs | - -SOURCES — карта, не требование прочитать все ссылки. Code показывает реализацию, -ADR — rationale, docs — текущие факты; старый пример не отменяет контракт. -Исследование предлагает варианты, а actual source разрешает выбор. При missing -или conflicting security/data facts сначала установи источник. Для обычной -работы OpenSpec/SDD не требуется. diff --git a/.ai/architecture.md b/.ai/architecture.md index 48724cd..a9a2311 100644 --- a/.ai/architecture.md +++ b/.ai/architecture.md @@ -1,4 +1,6 @@ -# Backend mental model +# Backend architecture + +## Устройство и data flow NestJS + TypeScript modular monolith. Composition root `AppModule` подключает бизнес-модули, validated configuration, Prisma и Pino. Общая `configureApp` @@ -19,7 +21,7 @@ HTTP controller → application service/use case → pure domain rules presentation/application/domain/infrastructure; компактные остаются плоскими. `*.module.ts` — Nest DI, `*.api.ts` — используемые cross-module exports. Application использует конкретные providers; обязательных ports на каждый -класс нет. [Module convention](conventions/backend-architecture.md). +класс нет. Stocks/bonds — slices одного instruments module: catalog владеет identity/cache, market — котировками, income — выплатами/личными позициями. Общая orchestration @@ -29,7 +31,7 @@ domain из-за extraction в функцию. Boundary mappers преобраз PrismaService владеет DB lifecycle. Самостоятельные use cases получают данные через concrete repositories; существующие связные CRUD/transaction services -сохраняют прямые queries по [data convention](conventions/backend-data.md). +сохраняют прямые queries по [data convention](conventions/data.md). Money — currency + decimal string; расчёты используют точные общие helpers. Auth управляет identity/session; TbankAccessService проверяет доступ к счёту и @@ -41,4 +43,220 @@ Runtime OpenAPI из Nest → предоставленный frontend snapshot Problem Details фильтруются общей HTTP boundary; StructuredLogger добавляет request context и ограничивает поля событий. Jest unit tests, изолированные SQLite/HTTP suites и OpenAPI parity доказывают разные части поведения. -Системные потоки и ADR выбираются через [SOURCES](SOURCES.md). +Системные потоки и ADR выбираются через [Источники](../AGENTS.md#источники). + +## Module-first organization + +Сначала выбери business owner в `modules/`, затем ответственность внутри него. + +### Progressive structure + +Три уровня организации: business module → responsibility layer → technical grouping +при реальной необходимости. Компактный module с понятными controller/DTO/service +может оставаться плоским. Ориентир 5–7 файлов не является порогом: учитывай число +ответственностей, наличие domain/repository/use cases и удобство просмотра каталога. + +Когда в module смешаны несколько services, transport, расчёты и persistence, +группируй файлы по ответственности: + +```text +feature/ +├── feature.module.ts +├── feature.api.ts # только при существующих внешних потребителях +├── presentation/ # HTTP, DTO, guards +├── application/ # сценарии, orchestration, application input +├── domain/ # чистые бизнес-правила и расчёты +└── infrastructure/ # persistence, framework и boundary mapping +``` + +Создавай только нужные слои. В корне сложного module оставляй composition и +публичную boundary; production code и colocated tests размещай в слое владельца. +Внутри business slice действует то же правило, без отдельного Nest module ради папок. +`controllers/`, `dto/`, `services/`, `use-cases/`, `persistence/`, `mappers/` +допустимы только внутри слоя, когда множество однотипных файлов мешает навигации. +Для одного controller или пары DTO дополнительная поддиректория не нужна. +Пустые папки, новые barrels, CQRS и interfaces не требуются для этой организации. + +## Поиск и создание файла + +При работе над feature проходи владельцев в порядке: + +1. Найди business module. +2. Прочитай его `*.module.ts` (для slice — composition родительского module). +3. Проверь `presentation/`: controller и transport contract. +4. Проследи вызовы в `application/`: сценарии и services. +5. Проверь используемые правила в `domain/`. +6. Проверь `infrastructure/`: repository и технические реализации. +7. По обнаруженным зависимостям переходи в `integrations/` и `common/`. + +В плоском module классифицируй файлы по suffix и следуй их ответственности. +Отсутствующий слой пропускай. Если business owner известен, начинай локально, +а не с repository-wide search. + +**Rule of locality:** code that belongs to one business capability stays inside +that capability unless it is cross-cutting. Например, расчёты карт принадлежат +bank-cards/domain, даже если реализованы обычными функциями. + +Перед созданием файла определи: + +1. Какой business module владеет изменением? Сначала owner, затем common при + подтверждённой общей потребности. +2. Какова ответственность: HTTP/DTO/guard → presentation; сценарий/orchestration → + application; чистое business rule/calculation → domain; persistence или + module-specific technical implementation → infrastructure; внешний протокол → integrations. +3. Какой approved suffix из таблицы соответствует этой ответственности? +4. Какой слой владеет файлом? В компактном плоском module сохрани размещение рядом + с owner; в слоистом используй соответствующий слой. +5. Достаточно ли colocate файл рядом с существующими? Level 3 grouping создавай + только при заметном улучшении навигации по множеству однотипных файлов. + +Если owner или роль не определены, сначала установи ответственность. Generic +`utils.ts`, `helpers.ts`, `manager.ts`, `handler.ts` и arbitrary directory её не заменяют. + +## Naming & File Suffix Convention + +Имена файлов — `kebab-case`. File suffix определяет responsibility, directory +определяет architectural layer. Размещение в таблице предпочтительно для +слоистого module; компактный плоский module сохраняет файлы в корне. +Cross-module consumers во всех строках ограничены публичной границей ниже. + +| Suffix | Когда использовать | Где | Кто зависит | Когда не использовать | +| --- | --- | --- | --- | --- | +| `*.controller.ts` | HTTP endpoint / transport | presentation | Nest wiring | DB, расчёты, SDK, сложная оркестрация | +| `*.dto.ts` | Request/response, validation, OpenAPI | presentation | Controller; application boundary при совпадении shape | Универсальная domain model | +| `*.use-case.ts` | Один самостоятельный application scenario | application | Controller, другой orchestrator | Prisma queries, Request/Response, SDK implementation | +| `*.service.ts` | Конкретная cohesive reusable capability | рядом с owner; application/domain/integration по роли | Сценарии и services; domain — только от чистого domain service | Свалка несвязанных сценариев | +| `*.repository.ts` | Persistence queries и согласованные DB reads/writes | infrastructure | Application service/use case | Domain calculations, HTTP request, external transport | +| `*.client.ts` | Низкоуровневый внешний HTTP/gRPC/SDK | integrations или infrastructure | Application orchestration, boundary mapper | Пользовательский lifecycle и domain rules | +| `*.mapper.ts` | Meaningful conversion между моделями | обычно infrastructure у преобразуемой boundary | Код соответствующей boundary | Формальное копирование нескольких одинаковых полей | +| `*.policy.ts` | Cohesive decision rule | domain/application/integration по входным данным | Владелец правила; domain — от чистой policy | Persistence, transport, generic strategy | +| `*.guard.ts` | Nest guard | presentation | Nest/controller | Основная account authorization и финансовые правила | +| `*.strategy.ts` | Framework strategy, например JWT | infrastructure | Framework wiring | Обычный service без strategy contract | +| `*.module.ts` | Nest imports/providers/controllers/exports | module root | Composition root, importing module | Бизнес-логика | +| `*.types.ts` | Малая группа связанных TS types | рядом с owner | Потребители модели на разрешённых слоях | Глобальные types всего module, runtime validation | +| `*.api.ts` | Явный список публичных exports | root конкретного module/integration | Другие modules | DTO/client/service implementation или generic API logic | +| `*-input.ts` | Application input: нормализация и проверка формы, отличающейся от wire DTO | application | Application code | Копия DTO под новым именем; вариант `*.input.ts` | +| `*.calculations.ts`, `*.rules.ts` или предметное имя | Чистые бизнес-расчёты, value semantics | domain | Application и domain | Nest/Prisma/HTTP/SDK зависимости | +| `*.spec.ts` | Существующая unit/component проверка поведения | рядом с owner/slice | Test runner | Архитектурные проверки папок и trivial wiring | + +`bank-card-input.ts` использует выбранный `*-input.ts`: переводит Money в +копейки, нормализует значения и проверяет application input. DTO не дублируется +отдельным Input type. `rub-money.ts` — domain-правила RUB; `bond-money.mapper.ts` +— преобразование ответа клиента. Эти роли не определяются словом money в имени. + +Технические framework suffixes (`*.filter.ts`, `*.middleware.ts`, +`*.decorator.ts`, `*.config.ts`, `*.validation.ts`) сохраняют соответствующие +существующие роли в common/config. `tbank.crypto.ts` — локальная реализация +шифрования токена; имя не является разрешением создавать generic crypto layer. +HTTP/e2e naming и расположение — в [Testing](conventions/testing.md). + +Без зафиксированной необходимости не вводи `*.helper.ts`, `*.utils.ts`, +`*.manager.ts`, `*.processor.ts`, `*.handler.ts`, `*.provider.ts`, `*.factory.ts`. +Новая naming convention сначала получает конкретную роль и обоснование здесь. +Для data access выбран только `*.repository.ts`, не persistence/store/dao. + +## Публичные границы + +`*.module.ts` задаёт DI boundary; `*.api.ts` перечисляет используемые другими +модулями providers, функции и типы. Внешний production import идёт через этот +API или файл Nest module. Чистый `integrations/tbank/decimal.ts` — отдельный +публичный вход для protobuf conversion: импорт арифметики не загружает DI/Prisma. +Внутри модуля используй прямые локальные импорты. +Tests могут импортировать внутренний owner для проверки поведения и mocks. +Новый export требует текущего потребителя; controller и persistence остаются +приватными. Не добавляй `index.ts` на каждом уровне. + +Circular dependencies устраняй изменением ownership/composition. `forwardRef()` +не является стандартным исправлением boundaries; исключение требует конкретной +неизбежной связи и проверки DI initialization. + +Каждый provider регистрируется у одного владельца. Потребители импортируют +его module, а не повторяют provider: это сохраняет единственный кэш и лимитер. +`AuthModule` экспортирует guard; наличие JWT не заменяет account access check. + +- `income` читает операции через `operations.api.ts`. +- `instruments` получает личные позиции через `portfolio.api.ts`. +- `tbank-connection` использует T-Bank integration и публичный сброс портфеля. +- `portfolio` и другие инвестиционные модули используют `TbankModule`, который + не зависит от business modules. Так управление подключением не образует цикл. +- `investment-calendar.ts` и `account-reference.dto.ts` разделяются только + инвестиционными областями; это не глобальная библиотека бизнес-правил. + +## Ответственность и направление зависимостей + +| Владелец | Ответственность | +| --- | --- | +| Controller / presentation | Route, verified identity, transport DTO, validation, status/cookie; вызов сценария | +| Use case / application service | Оркестрация, business validation, доступ и согласованность результата | +| Domain | Чистые правила и расчёты; без NestJS, Prisma, HTTP/Swagger и внешних SDK | +| Persistence / infrastructure | Запросы, транзакции, технические реализации границ | +| `src/integrations/{tbank,moex,ncr}` | Внешние протоколы, декодирование, retry/deadline/cache; без зависимостей от HTTP DTO business modules | +| `src/prisma` | Общий DB provider и lifecycle; не generic repository | + +Основной вызов: presentation → application → domain; application использует +конкретные infrastructure providers, зарегистрированные module. Infrastructure +может использовать application types/input и domain rules; новые interfaces +ради инверсии зависимостей не обязательны. Domain не импортирует presentation или +infrastructure. Существующие связные CRUD/transaction services могут сохранять прямые queries +по [Data](conventions/data.md); use case получает данные через repository и сам +Prisma queries не выполняет. Entity/interface на каждую таблицу не требуется. +DTO остаются транспортными runtime classes; существующее использование DTO +на application boundary допустимо по [HTTP API](conventions/http-api.md). + +Сохраняй существующий error contract: HTTP exceptions остаются на application/ +transport boundary. Чистому расчёту не нужен новый framework ошибок. +Mapping и stable codes — по [Errors](conventions/error-handling.md). +Точные ограничения — [HTTP](conventions/http-api.md), [Data](conventions/data.md), +[Security](conventions/security-auth.md), [Integrations](conventions/integrations.md). + +## Сценарии и размер сервисов + +Разделяй независимые операции, а не строки. Пример: `BankCardsService` владеет +картами, `BankOperationsService` — журналом/переводами, `GetBankSummaryUseCase` — +сводкой через repository и чистый расчёт. Не разрывай денежную transaction, +общий cache/limiter или pending promises при extraction. + +Расширение abstractions — по [change scope](../../.ai/conventions/code-health.md#change-scope). + +## Common + +`common` содержит общий error filter, logging, API decorators, тип `Money`, +его HTTP `MoneyDto` и точную decimal-арифметику, используемые несколькими областями. Код одного +модуля принадлежит этому модулю: например, RUB/kopecks rules — банковским картам, +protobuf units/nano conversion — T-Bank integration. Domain использует +`money.types.ts`, а не Swagger DTO; это общая форма денег для карт и портфеля. +Давай функциям предметные имена; не создавай глобальные `utils.ts`/`helpers.ts`. + +## Domain literals and magic values + +Общий выбор symbolic values и числовых policies — +[Domain values](../../.ai/conventions/code-health.md#domain-values). +Сохраняй strict TS и explicit boundary types. Для недоверенного input выбирай +`unknown` + runtime narrowing; отключённый lint `no-explicit-any` не делает +`any` нормой domain/application model. `declare` допустим для заполняемых извне +DTO fields, не для сокрытия неинициализированного owned state. + +OpenAPI enum metadata и runtime validation должны читать тот же vocabulary, +что application/domain. Например, `BankOperationKind` принадлежит bank-cards/domain, +а DTO получает допустимые значения через `Object.values(BankOperationKind)`. +Frontend переиспользует generated OpenAPI types/values по возможностям генератора, +а не определяет независимый backend-owned словарь. Внешние/generated enums +сохраняй; generated Prisma/protocol code вручную не меняй. + +`@typescript-eslint/no-magic-numbers` включён как error; базовое ESLint rule +отключено в пользу TS-версии. Исключения для очевидных чисел и их bigint форм, +индексов, default values, enum/type declarations и readonly properties заданы +в ESLint config. Test fixtures/expected values остаются inline, остальные lint +правила продолжают действовать. Numeric policy получает предметное имя и единицы; +semantic magic strings проверяются conventions/review без нового инструмента. + +## Logging + +События, levels, correlation и safe fields — по +[Observability](conventions/observability.md). + +## Проверка + +`npm run typecheck:app`, `npm run lint`, targeted Jest по изменённому use case. +Смысл boundaries проверяется review: backend не имеет отдельного автоматического +FSD/DI architecture checker. Existing tests и build не доказывают правильный owner. diff --git a/.ai/conventions/backend-architecture.md b/.ai/conventions/backend-architecture.md deleted file mode 100644 index 10734e8..0000000 --- a/.ai/conventions/backend-architecture.md +++ /dev/null @@ -1,218 +0,0 @@ -# Backend architecture - -## Module-first organization - -Устройство и data flow — в [mental model](../architecture.md). -Сначала выбери business owner в `modules/`, затем ответственность внутри него. - -### Progressive structure - -Три уровня организации: business module → responsibility layer → technical grouping -при реальной необходимости. Компактный module с понятными controller/DTO/service -может оставаться плоским. Ориентир 5–7 файлов не является порогом: учитывай число -ответственностей, наличие domain/repository/use cases и удобство просмотра каталога. - -Когда в module смешаны несколько services, transport, расчёты и persistence, -группируй файлы по ответственности: - -```text -feature/ -├── feature.module.ts -├── feature.api.ts # только при существующих внешних потребителях -├── presentation/ # HTTP, DTO, guards -├── application/ # сценарии, orchestration, application input -├── domain/ # чистые бизнес-правила и расчёты -└── infrastructure/ # persistence, framework и boundary mapping -``` - -Создавай только нужные слои. В корне сложного module оставляй composition и -публичную boundary; production code и colocated tests размещай в слое владельца. -Внутри business slice действует то же правило, без отдельного Nest module ради папок. -`controllers/`, `dto/`, `services/`, `use-cases/`, `persistence/`, `mappers/` -допустимы только внутри слоя, когда множество однотипных файлов мешает навигации. -Для одного controller или пары DTO дополнительная поддиректория не нужна. -Пустые папки, новые barrels, CQRS и interfaces не требуются для этой организации. - -## Поиск и создание файла - -При работе над feature проходи владельцев в порядке: - -1. Найди business module. -2. Прочитай его `*.module.ts` (для slice — composition родительского module). -3. Проверь `presentation/`: controller и transport contract. -4. Проследи вызовы в `application/`: сценарии и services. -5. Проверь используемые правила в `domain/`. -6. Проверь `infrastructure/`: repository и технические реализации. -7. По обнаруженным зависимостям переходи в `integrations/` и `common/`. - -В плоском module классифицируй файлы по suffix и следуй их ответственности. -Отсутствующий слой пропускай. Если business owner известен, начинай локально, -а не с repository-wide search. - -**Rule of locality:** code that belongs to one business capability stays inside -that capability unless it is cross-cutting. Например, расчёты карт принадлежат -bank-cards/domain, даже если реализованы обычными функциями. - -Перед созданием файла определи: - -1. Какой business module владеет изменением? Сначала owner, затем common при - подтверждённой общей потребности. -2. Какова ответственность: HTTP/DTO/guard → presentation; сценарий/orchestration → - application; чистое business rule/calculation → domain; persistence или - module-specific technical implementation → infrastructure; внешний протокол → integrations. -3. Какой approved suffix из таблицы соответствует этой ответственности? -4. Какой слой владеет файлом? В компактном плоском module сохрани размещение рядом - с owner; в слоистом используй соответствующий слой. -5. Достаточно ли colocate файл рядом с существующими? Level 3 grouping создавай - только при заметном улучшении навигации по множеству однотипных файлов. - -Если owner или роль не определены, сначала установи ответственность. Generic -`utils.ts`, `helpers.ts`, `manager.ts`, `handler.ts` и arbitrary directory её не заменяют. - -## Naming & File Suffix Convention - -Имена файлов — `kebab-case`. File suffix определяет responsibility, directory -определяет architectural layer. Размещение в таблице предпочтительно для -слоистого module; компактный плоский module сохраняет файлы в корне. -Cross-module consumers во всех строках ограничены публичной границей ниже. - -| Suffix | Когда использовать | Где | Кто зависит | Когда не использовать | -| --- | --- | --- | --- | --- | -| `*.controller.ts` | HTTP endpoint / transport | presentation | Nest wiring | DB, расчёты, SDK, сложная оркестрация | -| `*.dto.ts` | Request/response, validation, OpenAPI | presentation | Controller; application boundary при совпадении shape | Универсальная domain model | -| `*.use-case.ts` | Один самостоятельный application scenario | application | Controller, другой orchestrator | Prisma queries, Request/Response, SDK implementation | -| `*.service.ts` | Конкретная cohesive reusable capability | рядом с owner; application/domain/integration по роли | Сценарии и services; domain — только от чистого domain service | Свалка несвязанных сценариев | -| `*.repository.ts` | Persistence queries и согласованные DB reads/writes | infrastructure | Application service/use case | Domain calculations, HTTP request, external transport | -| `*.client.ts` | Низкоуровневый внешний HTTP/gRPC/SDK | integrations или infrastructure | Application orchestration, boundary mapper | Пользовательский lifecycle и domain rules | -| `*.mapper.ts` | Meaningful conversion между моделями | обычно infrastructure у преобразуемой boundary | Код соответствующей boundary | Формальное копирование нескольких одинаковых полей | -| `*.policy.ts` | Cohesive decision rule | domain/application/integration по входным данным | Владелец правила; domain — от чистой policy | Persistence, transport, generic strategy | -| `*.guard.ts` | Nest guard | presentation | Nest/controller | Основная account authorization и финансовые правила | -| `*.strategy.ts` | Framework strategy, например JWT | infrastructure | Framework wiring | Обычный service без strategy contract | -| `*.module.ts` | Nest imports/providers/controllers/exports | module root | Composition root, importing module | Бизнес-логика | -| `*.types.ts` | Малая группа связанных TS types | рядом с owner | Потребители модели на разрешённых слоях | Глобальные types всего module, runtime validation | -| `*.api.ts` | Явный список публичных exports | root конкретного module/integration | Другие modules | DTO/client/service implementation или generic API logic | -| `*-input.ts` | Application input: нормализация и проверка формы, отличающейся от wire DTO | application | Application code | Копия DTO под новым именем; вариант `*.input.ts` | -| `*.calculations.ts`, `*.rules.ts` или предметное имя | Чистые бизнес-расчёты, value semantics | domain | Application и domain | Nest/Prisma/HTTP/SDK зависимости | -| `*.spec.ts` | Существующая unit/component проверка поведения | рядом с owner/slice | Test runner | Архитектурные проверки папок и trivial wiring | - -`bank-card-input.ts` использует выбранный `*-input.ts`: переводит Money в -копейки, нормализует значения и проверяет application input. DTO не дублируется -отдельным Input type. `rub-money.ts` — domain-правила RUB; `bond-money.mapper.ts` -— преобразование ответа клиента. Эти роли не определяются словом money в имени. - -Технические framework suffixes (`*.filter.ts`, `*.middleware.ts`, -`*.decorator.ts`, `*.config.ts`, `*.validation.ts`) сохраняют соответствующие -существующие роли в common/config. `tbank.crypto.ts` — локальная реализация -шифрования токена; имя не является разрешением создавать generic crypto layer. -HTTP/e2e naming и расположение — в [Testing](backend-testing.md). - -Без зафиксированной необходимости не вводи `*.helper.ts`, `*.utils.ts`, -`*.manager.ts`, `*.processor.ts`, `*.handler.ts`, `*.provider.ts`, `*.factory.ts`. -Новая naming convention сначала получает конкретную роль и обоснование здесь. -Для data access выбран только `*.repository.ts`, не persistence/store/dao. - -## Публичные границы - -`*.module.ts` задаёт DI boundary; `*.api.ts` перечисляет используемые другими -модулями providers, функции и типы. Внешний production import идёт через этот -API или файл Nest module. Чистый `integrations/tbank/decimal.ts` — отдельный -публичный вход для protobuf conversion: импорт арифметики не загружает DI/Prisma. -Внутри модуля используй прямые локальные импорты. -Tests могут импортировать внутренний owner для проверки поведения и mocks. -Новый export требует текущего потребителя; controller и persistence остаются -приватными. Не добавляй `index.ts` на каждом уровне. - -Circular dependencies устраняй изменением ownership/composition. `forwardRef()` -не является стандартным исправлением boundaries; исключение требует конкретной -неизбежной связи и проверки DI initialization. - -Каждый provider регистрируется у одного владельца. Потребители импортируют -его module, а не повторяют provider: это сохраняет единственный кэш и лимитер. -`AuthModule` экспортирует guard; наличие JWT не заменяет account access check. - -- `income` читает операции через `operations.api.ts`. -- `instruments` получает личные позиции через `portfolio.api.ts`. -- `tbank-connection` использует T-Bank integration и публичный сброс портфеля. -- `portfolio` и другие инвестиционные модули используют `TbankModule`, который - не зависит от business modules. Так управление подключением не образует цикл. -- `investment-calendar.ts` и `account-reference.dto.ts` разделяются только - инвестиционными областями; это не глобальная библиотека бизнес-правил. - -## Ответственность и направление зависимостей - -| Владелец | Ответственность | -| --- | --- | -| Controller / presentation | Route, verified identity, transport DTO, validation, status/cookie; вызов сценария | -| Use case / application service | Оркестрация, business validation, доступ и согласованность результата | -| Domain | Чистые правила и расчёты; без NestJS, Prisma, HTTP/Swagger и внешних SDK | -| Persistence / infrastructure | Запросы, транзакции, технические реализации границ | -| `src/integrations/{tbank,moex,ncr}` | Внешние протоколы, декодирование, retry/deadline/cache; без зависимостей от HTTP DTO business modules | -| `src/prisma` | Общий DB provider и lifecycle; не generic repository | - -Основной вызов: presentation → application → domain; application использует -конкретные infrastructure providers, зарегистрированные module. Infrastructure -может использовать application types/input и domain rules; новые interfaces -ради инверсии зависимостей не обязательны. Domain не импортирует presentation или -infrastructure. Существующие связные CRUD/transaction services могут сохранять прямые queries -по [Data](backend-data.md); use case получает данные через repository и сам -Prisma queries не выполняет. Entity/interface на каждую таблицу не требуется. -DTO остаются транспортными runtime classes; существующее использование DTO -на application boundary допустимо по [HTTP API](backend-http-api.md). - -Сохраняй существующий error contract: HTTP exceptions остаются на application/ -transport boundary. Чистому расчёту не нужен новый framework ошибок. -Mapping и stable codes — по [Errors](backend-error-handling.md). -Точные ограничения — [HTTP](backend-http-api.md), [Data](backend-data.md), -[Security](security-auth.md), [Integrations](backend-integrations.md). - -## Сценарии и размер сервисов - -Разделяй независимые операции, а не строки. Пример: `BankCardsService` владеет -картами, `BankOperationsService` — журналом/переводами, `GetBankSummaryUseCase` — -сводкой через repository и чистый расчёт. Не разрывай денежную transaction, -общий cache/limiter или pending promises при extraction. - -Расширение abstractions — по [change scope](../../../.ai/conventions/code-health.md#change-scope). - -## Common - -`common` содержит общий error filter, logging, API decorators, тип `Money`, -его HTTP `MoneyDto` и точную decimal-арифметику, используемые несколькими областями. Код одного -модуля принадлежит этому модулю: например, RUB/kopecks rules — банковским картам, -protobuf units/nano conversion — T-Bank integration. Domain использует -`money.types.ts`, а не Swagger DTO; это общая форма денег для карт и портфеля. -Давай функциям предметные имена; не создавай глобальные `utils.ts`/`helpers.ts`. - -## Domain literals and magic values - -Общий выбор symbolic values и числовых policies — -[Domain values](../../../.ai/conventions/code-health.md#domain-values). -Сохраняй strict TS и explicit boundary types. Для недоверенного input выбирай -`unknown` + runtime narrowing; отключённый lint `no-explicit-any` не делает -`any` нормой domain/application model. `declare` допустим для заполняемых извне -DTO fields, не для сокрытия неинициализированного owned state. - -OpenAPI enum metadata и runtime validation должны читать тот же vocabulary, -что application/domain. Например, `BankOperationKind` принадлежит bank-cards/domain, -а DTO получает допустимые значения через `Object.values(BankOperationKind)`. -Frontend переиспользует generated OpenAPI types/values по возможностям генератора, -а не определяет независимый backend-owned словарь. Внешние/generated enums -сохраняй; generated Prisma/protocol code вручную не меняй. - -`@typescript-eslint/no-magic-numbers` включён как error; базовое ESLint rule -отключено в пользу TS-версии. Исключения для очевидных чисел и их bigint форм, -индексов, default values, enum/type declarations и readonly properties заданы -в ESLint config. Test fixtures/expected values остаются inline, остальные lint -правила продолжают действовать. Numeric policy получает предметное имя и единицы; -semantic magic strings проверяются conventions/review без нового инструмента. - -## Logging - -События, levels, correlation и safe fields — по -[Observability](backend-observability.md). - -## Проверка - -`npm run typecheck:app`, `npm run lint`, targeted Jest по изменённому use case. -Смысл boundaries проверяется review: backend не имеет отдельного автоматического -FSD/DI architecture checker. Existing tests и build не доказывают правильный owner. diff --git a/.ai/conventions/backend-data.md b/.ai/conventions/data.md similarity index 98% rename from .ai/conventions/backend-data.md rename to .ai/conventions/data.md index bd6cf5b..83a044c 100644 --- a/.ai/conventions/backend-data.md +++ b/.ai/conventions/data.md @@ -15,7 +15,7 @@ PrismaService — существующий shared DB provider с lifecycle пр | Независимые reads | Не добавляй transaction ради внешнего вида | | Общая persistence abstraction | Только при реальной общей ответственности; generic repository для одной Prisma реализации не нужен | -Размещение repository определяется [Architecture](backend-architecture.md): +Размещение repository определяется [Architecture](../architecture.md): в слоистом module это `infrastructure/`. Repository — конкретный класс `*.repository.ts` у business owner, без обязательных interface/port/adapter. Он владеет queries, includes/filtering, DB mapping и @@ -81,4 +81,4 @@ client обновляй из schema. Не переписывай уже прим `npx prisma validate`; после schema change `npx prisma generate` и `npm run typecheck:app`. Invariants/atomicity/migration safety требуют tests на соответствующей boundary: mock Prisma не доказывает реальную transaction. -Выбор integration/e2e и известные ограничения — в [Testing](backend-testing.md). +Выбор integration/e2e и известные ограничения — в [Testing](testing.md). diff --git a/.ai/conventions/backend-error-handling.md b/.ai/conventions/error-handling.md similarity index 95% rename from .ai/conventions/backend-error-handling.md rename to .ai/conventions/error-handling.md index 7072d38..5c417dc 100644 --- a/.ai/conventions/backend-error-handling.md +++ b/.ai/conventions/error-handling.md @@ -29,7 +29,7 @@ error framework/registry. Если domain outcome требует HTTP interpreta SQL, credentials или raw upstream payload в ответе. - External auth failure и временная недоступность имеют разные outcomes; translation выполняется на existing integration/access boundary. -- Логи используют [observability](backend-observability.md). Redaction не +- Логи используют [observability](observability.md). Redaction не делает произвольное сообщение ошибки безопасным. ## Проверка @@ -37,4 +37,4 @@ error framework/registry. Если domain outcome требует HTTP interpreta Для изменённого кода проверь observable status/code и сохранность данных. Для validation нужен request через `configureApp`: fabricated `ValidationError` в unit test filter не доказывает форму, выходящую из реального ValidationPipe. -OpenAPI parity проверяет schema, а не response pipeline; [Testing](backend-testing.md). +OpenAPI parity проверяет schema, а не response pipeline; [Testing](testing.md). diff --git a/.ai/conventions/backend-http-api.md b/.ai/conventions/http-api.md similarity index 95% rename from .ai/conventions/backend-http-api.md rename to .ai/conventions/http-api.md index fcb1022..81e3353 100644 --- a/.ai/conventions/backend-http-api.md +++ b/.ai/conventions/http-api.md @@ -27,7 +27,7 @@ module или presentation. DTO можно передать application boundary формы; отдельный Input с теми же полями не нужен. Domain не импортирует DTO: например, чистые денежные расчёты используют `Money`, а Swagger — `MoneyDto`. Нормализованная форма input нужна только при смысловом отличии от wire contract; -выбор suffix принадлежит [Architecture](backend-architecture.md). +выбор suffix принадлежит [Architecture](../architecture.md). ValidationPipe уже настроен в `configureApp` (`transform`, `whitelist`). Не обещай rejection неизвестных fields: `forbidNonWhitelisted` не включён. Для query numbers/arrays выбирай явное преобразование и валидаторы, как в @@ -37,7 +37,7 @@ ValidationPipe уже настроен в `configureApp` (`transform`, `whitelis Используй типизированные response DTO, `ApiOkResponse`/другой фактический status и существующий `ApiProblemResponses`. Prisma model не является public contract: response выбирает поля явно. Stable codes, RFC 9457 профиль и mapping принадлежат -[Errors](backend-error-handling.md). +[Errors](error-handling.md). **Безопасный путь:** используй exception и общий filter, а не локальный `res.status(...).json(...)`. Исключение — транспортная операция вроде cookie; @@ -64,4 +64,4 @@ helper ради первого endpoint. Новая pagination явно опре errors на boundary, где они возникают. Unit вызов controller не запускает pipes, guards или serialization. `npm run test:openapi-contract` проверяет schema parity, но не поведение invalid request; выбирай соответствующий test по -[Testing](backend-testing.md). Финальный gate не заменяет semantic review ошибок. +[Testing](testing.md). Финальный gate не заменяет semantic review ошибок. diff --git a/.ai/conventions/backend-integrations.md b/.ai/conventions/integrations.md similarity index 95% rename from .ai/conventions/backend-integrations.md rename to .ai/conventions/integrations.md index 9ade729..ebc2951 100644 --- a/.ai/conventions/backend-integrations.md +++ b/.ai/conventions/integrations.md @@ -18,7 +18,7 @@ RPC, credentials/access и resilience. Эти владельцы не объед `*.client.ts` содержит transport и его retries; `*.api.ts` только перечисляет публичные exports. Все шесть `auth/operations/portfolio/tbank/moex/ncr.api.ts` имеют одинаковую роль программной границы, определённую в -[Architecture](backend-architecture.md). Логика, DTO definitions и HTTP routes +[Architecture](../architecture.md). Логика, DTO definitions и HTTP routes в эти файлы не помещаются. Type exports обозначаются `export type`. Преобразование provider records происходит на boundary: например, @@ -31,7 +31,7 @@ client или mapper для простого копирования полей. Для нового account use case сначала используй TbankAccessService: access, connection generation и invalid-token translation не дублируются в callers. Mapper внешних финансовых значений остаётся внутри business capability, -обычно в infrastructure по [Architecture](backend-architecture.md). +обычно в infrastructure по [Architecture](../architecture.md). ## Retry, cache и protocol @@ -69,4 +69,4 @@ Generated artifacts, если есть, создавай из source, без р upstream responses. Проверь queue/deadline/retry limit, token translation, cache isolation и late response для затронутых ветвей. Fixtures синтетические; реальная сеть, секреты и пользовательские accounts не нужны. -Logging использует [existing logger](backend-architecture.md#logging). +Logging использует [existing logger](../architecture.md#logging). diff --git a/.ai/conventions/backend-observability.md b/.ai/conventions/observability.md similarity index 94% rename from .ai/conventions/backend-observability.md rename to .ai/conventions/observability.md index b3ff97c..ae028cb 100644 --- a/.ai/conventions/backend-observability.md +++ b/.ai/conventions/observability.md @@ -15,7 +15,7 @@ или upstream error. Secret в `message/stack` не становится безопасным из-за allowlist; правила [Security](security-auth.md) обязательны для sensitive data. - Для ошибки используй public code и безопасный context операции/попыток; - [error mapping](backend-error-handling.md) не отдаёт внутренние детали клиенту. + [error mapping](error-handling.md) не отдаёт внутренние детали клиенту. Причины — [ADR 008](../../../docs/adr/008-pino-backend-logging.md), системное поведение — [HTTP observability](../../../docs/architecture/http-api.md#наблюдаемость). diff --git a/.ai/conventions/security-auth.md b/.ai/conventions/security-auth.md index eafe2a9..f4ecf59 100644 --- a/.ai/conventions/security-auth.md +++ b/.ai/conventions/security-auth.md @@ -55,6 +55,6 @@ cache invalidation при замене/deletion и ограничение key ro фиктивным success или log. Logger allowlist/redaction не делает произвольный message безопасным. -Targeted auth/access/crypto/logging tests + [Testing](backend-testing.md). +Targeted auth/access/crypto/logging tests + [Testing](testing.md). Cookie/guard/HTTP changes требуют configured HTTP proof; frontend Bearer/retry проверяется MSW. Schema parity не доказывает отсутствие утечки secret. diff --git a/.ai/conventions/backend-testing.md b/.ai/conventions/testing.md similarity index 100% rename from .ai/conventions/backend-testing.md rename to .ai/conventions/testing.md diff --git a/AGENTS.md b/AGENTS.md index e35f322..2c50351 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,20 +2,19 @@ Общие инструкции — [workspace AGENTS](../AGENTS.md). Объединяй triggers задачи, путей и обнаруженных boundaries; читай только нужные темы до решения. -[Sources](.ai/SOURCES.md) выбирает факты/spec/ADR по возникшему вопросу. +Системные факты/spec/ADR выбирай в разделе [Источники](#источники) по вопросу. | Решение / сигнал | Читать | | --- | --- | -| Первое знакомство, data flow, общая архитектура | [Mental model](.ai/architecture.md) | -| Module/layer/public API/DI/suffix, owner, перенос responsibility | [Modules/application](.ai/conventions/backend-architecture.md) | -| TS vocabulary, domain literals, magic values | [Types/values](.ai/conventions/backend-architecture.md#domain-literals-and-magic-values) | -| Controller/DTO/validation, route/status/response, OpenAPI | [HTTP API](.ai/conventions/backend-http-api.md) | -| Application/domain/HTTP error, exception, code | [Errors](.ai/conventions/backend-error-handling.md) | -| Repository/Prisma/query, transaction, migration, atomic write | [Data](.ai/conventions/backend-data.md) | -| Tests или behavior с regression risk | [Testing](.ai/conventions/backend-testing.md) | +| Устройство/data flow, module/layer/public API/DI/suffix, owner | [Architecture](.ai/architecture.md) | +| TS vocabulary, domain literals, magic values | [Types/values](.ai/architecture.md#domain-literals-and-magic-values) | +| Controller/DTO/validation, route/status/response, OpenAPI | [HTTP API](.ai/conventions/http-api.md) | +| Application/domain/HTTP error, exception, code | [Errors](.ai/conventions/error-handling.md) | +| Repository/Prisma/query, transaction, migration, atomic write | [Data](.ai/conventions/data.md) | +| Tests или behavior с regression risk | [Testing](.ai/conventions/testing.md) | | JWT/cookie/session/guard/access, encryption, secrets | [Security](.ai/conventions/security-auth.md) | -| External client/gateway/SDK/proto, retry/rate limit/cache | [Integrations](.ai/conventions/backend-integrations.md) | -| Logging/context/levels, sensitive logs | [Observability](.ai/conventions/backend-observability.md); sensitive data также Security | +| External client/gateway/SDK/proto, retry/rate limit/cache | [Integrations](.ai/conventions/integrations.md) | +| Logging/context/levels, sensitive logs | [Observability](.ai/conventions/observability.md); sensitive data также Security | | Изменение публичного wire contract / Swagger metadata | HTTP API + [HTTP contract source](../docs/architecture/http-api.md#источник-контракта) | | Helper/dependency/новый pattern, refactor/scope | [Code health](../.ai/conventions/code-health.md), для пакета — [Dependencies](../.ai/conventions/dependencies.md) | | Выбор gate, tooling, завершение | [Guardrails](../.ai/conventions/guardrails.md) | @@ -25,3 +24,19 @@ Повтори routing по итоговому diff, включая новые файлы. Migration backlog читается только при соответствующем finding через [plan](../.ai/migrations/conventions-v1/plan.md). + +## Источники + +Открывай только источник по возникшему вопросу. Code показывает реализацию, +docs — текущую систему, ADR — rationale, specs — requirements. Старый пример +не отменяет контракт; conflicting security/data facts выясняй до решения. + +| Вопрос | Источник | +| --- | --- | +| HTTP errors/DTO/wire shape | [HTTP API](../docs/architecture/http-api.md); editable OpenAPI генерирует Nest | +| Auth/session, account access, secrets | [Authentication](../docs/domains/authentication/overview.md), [T-Bank](../docs/integrations/tbank.md) | +| Prisma/data ownership, хранение и доступ | [Containers](../docs/architecture/containers.md#хранимые-данные-и-границы-доступа) | +| T-Bank/cache/logging | [T-Bank](../docs/integrations/tbank.md), [Freshness](../docs/architecture/cross-cutting/data-freshness.md), [Observability](../docs/architecture/http-api.md#наблюдаемость) | +| Money, account/position/income/cache meanings | [Глоссарий](../docs/glossary.md) | +| Поведение области / требования | [Области docs](../docs/README.md#предметные-области), [Specs index](../specs/AI_INDEX.md) | +| Versions/scripts/check scope | Manifest/lockfile, ESLint/TypeScript/Jest/Prisma configs | diff --git a/README.md b/README.md index 783090f..1d5d6a3 100644 --- a/README.md +++ b/README.md @@ -78,10 +78,10 @@ runtime-валидацию. В обычном коде нужны инициал ## Документация -- [Router conventions](.ai/CONVENTIONS.md) и [источники](.ai/SOURCES.md) -- [Backend architecture](.ai/conventions/backend-architecture.md) -- [HTTP API](.ai/conventions/backend-http-api.md), [data](.ai/conventions/backend-data.md) и [testing](.ai/conventions/backend-testing.md) -- [Security/auth](.ai/conventions/security-auth.md) и [integrations](.ai/conventions/backend-integrations.md) +- [AGENTS: правила и источники](AGENTS.md) +- [Backend architecture](.ai/architecture.md) +- [HTTP API](.ai/conventions/http-api.md), [data](.ai/conventions/data.md) и [testing](.ai/conventions/testing.md) +- [Security/auth](.ai/conventions/security-auth.md) и [integrations](.ai/conventions/integrations.md) - [Карта проектных фактов](../docs/README.md) - [API contract и runtime OpenAPI](../docs/architecture/http-api.md#источник-контракта) -- 2.54.0 From 32c12fb10d4f739c6af0ce32b8f2ea6e94c42090 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Tue, 15 Sep 2026 20:45:39 +0300 Subject: [PATCH 3/6] feat: generate OpenAPI without peer repositories --- .ai/architecture.md | 12 ++--- .ai/conventions/data.md | 12 ++--- .ai/conventions/engineering.md | 74 +++++++++++++++++++++++++++++ .ai/conventions/error-handling.md | 4 +- .ai/conventions/http-api.md | 29 ++++++++++-- .ai/conventions/integrations.md | 17 +++---- .ai/conventions/observability.md | 2 - .ai/conventions/security-auth.md | 23 ++++----- .ai/conventions/testing.md | 6 +-- .ci/refs.json | 3 -- .gitea/workflows/verify.yml | 32 ++----------- .gitignore | 3 ++ AGENTS.md | 36 +++++++------- README.md | 79 ++++++++++++++++++++++++------- package.json | 5 +- scripts/generate-openapi.cjs | 40 ++++++++++++++++ src/main.ts | 13 ++--- src/openapi.ts | 11 +++++ test/openapi-contract.e2e-spec.ts | 67 ++++++++++++++------------ 19 files changed, 317 insertions(+), 151 deletions(-) create mode 100644 .ai/conventions/engineering.md delete mode 100644 .ci/refs.json create mode 100644 scripts/generate-openapi.cjs create mode 100644 src/openapi.ts diff --git a/.ai/architecture.md b/.ai/architecture.md index a9a2311..b89cd57 100644 --- a/.ai/architecture.md +++ b/.ai/architecture.md @@ -39,11 +39,12 @@ connection generation. Integration clients владеют protocol/deadline/retr provider records преобразуются перед чистыми domain calculations. Пользовательский `tbank-connection` не владеет низкоуровневым gRPC client. -Runtime OpenAPI из Nest → предоставленный frontend snapshot → generated types. +Nest DTO/metadata → `npm run openapi:generate` → игнорируемый Git `openapi.json` +для публичных consumers. Problem Details фильтруются общей HTTP boundary; StructuredLogger добавляет request context и ограничивает поля событий. Jest unit tests, изолированные SQLite/HTTP suites и OpenAPI parity доказывают разные части поведения. -Системные потоки и ADR выбираются через [Источники](../AGENTS.md#источники). +Локальные источники выбираются через [Источники](../AGENTS.md#источники). ## Module-first organization @@ -216,7 +217,7 @@ Mapping и stable codes — по [Errors](conventions/error-handling.md). сводкой через repository и чистый расчёт. Не разрывай денежную transaction, общий cache/limiter или pending promises при extraction. -Расширение abstractions — по [change scope](../../.ai/conventions/code-health.md#change-scope). +Расширение abstractions — по [change scope](conventions/engineering.md#change-scope). ## Common @@ -230,7 +231,7 @@ protobuf units/nano conversion — T-Bank integration. Domain используе ## Domain literals and magic values Общий выбор symbolic values и числовых policies — -[Domain values](../../.ai/conventions/code-health.md#domain-values). +[Domain values](conventions/engineering.md#domain-values). Сохраняй strict TS и explicit boundary types. Для недоверенного input выбирай `unknown` + runtime narrowing; отключённый lint `no-explicit-any` не делает `any` нормой domain/application model. `declare` допустим для заполняемых извне @@ -239,8 +240,7 @@ DTO fields, не для сокрытия неинициализированно OpenAPI enum metadata и runtime validation должны читать тот же vocabulary, что application/domain. Например, `BankOperationKind` принадлежит bank-cards/domain, а DTO получает допустимые значения через `Object.values(BankOperationKind)`. -Frontend переиспользует generated OpenAPI types/values по возможностям генератора, -а не определяет независимый backend-owned словарь. Внешние/generated enums +Публичные enum values входят в OpenAPI artifact. Внешние/generated enums сохраняй; generated Prisma/protocol code вручную не меняй. `@typescript-eslint/no-magic-numbers` включён как error; базовое ESLint rule diff --git a/.ai/conventions/data.md b/.ai/conventions/data.md index 83a044c..b79cc76 100644 --- a/.ai/conventions/data.md +++ b/.ai/conventions/data.md @@ -51,16 +51,16 @@ application checks. Предварительный lookup не заменяет Transaction не охватывает сетевой T-Bank вызов как будто это одна atomic DB операция. Планируй порядок и compensation по текущей задаче; не удерживай DB lock во время retry внешней сети. Access/cache generation semantics — у -[T-Bank source](../../../docs/integrations/tbank.md#граница-финансового-кэша). +[T-Bank integration](integrations.md#retry-cache-и-protocol). ## Деньги и точность Для денежных значений используй existing decimal helpers и строковый wire value, не JS floating-point арифметику для сумм. Currency — отдельная часть Money; разные валюты не складываются без явно заданного conversion contract. -Источник: [Money](../../../docs/glossary.md#money), -пример: [decimal.ts](../../src/common/decimal.ts). UI formatting не меняет -backend ownership расчётов и доменную семантику. +Источник формы — `src/common/money.types.ts`; +пример точной арифметики: [decimal.ts](../../src/common/decimal.ts). +Форматирование ответа не меняет доменную семантику и точность расчёта. ## Schema и migration @@ -73,8 +73,8 @@ client обновляй из schema. Не переписывай уже прим **Безопасный путь:** additive изменение, заполнение/проверка данных и только затем удаление прежней формы, если задача этого требует. Примеры schema/migrations: -[Prisma source](../../prisma/schema.prisma); исторический rationale — -[ADR 001](../../../docs/adr/001-prisma-sqlite-local-mvp.md), не текущий список таблиц. +[Prisma source](../../prisma/schema.prisma). SQLite — embedded storage текущего +приложения; файл БД и encryption key требуют согласованного backup/restore. ## Проверка diff --git a/.ai/conventions/engineering.md b/.ai/conventions/engineering.md new file mode 100644 index 0000000..98f5a1b --- /dev/null +++ b/.ai/conventions/engineering.md @@ -0,0 +1,74 @@ +# Работа в репозитории + +## Граница и порядок работы + +Репозиторий сам содержит инструкции, исходники, конфигурацию и входы своих +проверок. Сборка, CI и обязательный маршрут чтения работают без соседних +checkout. Внешний API задаётся предоставленным публичным контрактом; +исходники или инструкции его реализации не являются зависимостью разработки. +При нехватке требования сообщи, какое решение не определено, до его реализации. + +Начни с результата и affected areas. До edits проследи flow/invariants, найди +owner и 1–3 примера. Код показывает механизм; случайный аналог не меняет контракт. +Работай напрямую; OpenSpec/SDD lifecycle — только по явному запросу. +Перед Node-командами активируй местную `.nvmrc`: `nvm use --silent`. +Сохраняй чужие staged/unstaged/untracked изменения и read-only архивы. + +## Change scope + +Сначала расширь существующего владельца. Иначе используй standard library, +платформу или установленную dependency; небольшая новая реализация допустима +при собственной ответственности. Не создавай helper/interface/wrapper ради +возможного reuse. Общее имя не означает общую бизнес-ответственность. +Сохраняй validation, authorization, atomicity, accessibility и migration safety. + +После diff выполни Boy Scout review: проверь новое дублирование, owner, callers, +небольшие связанные legacy issues и соответствие local conventions. Исправляй +непосредственно связанные low-risk проблемы; широкий cleanup — отдельная задача. + +## Domain values + +Finite business vocabulary принадлежит одному domain/module/entity owner. +Переиспользуй его symbols; для нового словаря без аналога — `as const` + union. +Сохраняй wire values; generated/external enums не переписывай вручную. +Numeric policy называй по смыслу и единицам. Индексы, `Math.max(value, 0)` и +обычные labels не выноси в constants без semantic benefit. +Локальный TypeScript/lint профиль определяет дополнительные ограничения. + +## Dependencies + +Проверь аналоги и direct dependencies в местном manifest до добавления пакета. +Новая dependency оправдана текущей потребностью и меньшей общей сложностью; +оцени compatibility, runtime/bundle и maintenance/security. Импортируемый пакет +объявляется напрямую; транзитивная установка не является контрактом. +Сохраняй exact versions и lockfile; воспроизводимая установка — `npm ci`. +Peer-конфликт решай совместимыми версиями без `--force`/`--legacy-peer-deps`. +Используй существующий analyzer; не добавляй второй для той же проверки. + +## Test Intent + +Перед нетривиальным тестом кратко определи наблюдаемый исход, regression risk, +значимые edge/error/concurrency cases и самую дешёвую надёжную boundary. +Отдельный artifact не нужен. Проверяй результат, а не private call order, +декларации, тривиальные wrappers или поведение framework. +При bug сначала воспроизведи исход. Mock race не доказывает DB atomicity. +Уровни и runnable commands выбирай по местной testing convention. + +## Проверки и завершение + +Code/dependency change: `npm ci`, полный `npm run verify` и дополнительные +risk checks из местной testing convention. Targeted tests ускоряют итерацию, +но не заменяют финальный gate. `lint:fix`/format — edits, не proof. +Markdown-only: проверь новые/изменённые тексты, локальные ссылки/якоря, маршруты +чтения и `git diff --check`; application build ради текста не нужен. +Повтори routing по staged/unstaged/new files. Запиши краткие команды, результаты +и ограничения. Зелёный lint не заменяет semantic/security review. + +## Git и релизы + +Работа — в `codex/` от актуального `develop`, затем PR в `develop`. +Сохраняй чужие изменения; перед commit просмотри staged paths и diff. +Commits и PR titles — Conventional Commits; допустим `spec:`. Каждый commit +содержит одну связанную задачу. Root другого checkout не управляет этим repo. +Прямой push в `main`, force-push и перенос опубликованных тегов не входят в +обычную работу. Перед release/hotfix прочитай местный README, раздел «Релизы». diff --git a/.ai/conventions/error-handling.md b/.ai/conventions/error-handling.md index 5c417dc..7996c42 100644 --- a/.ai/conventions/error-handling.md +++ b/.ai/conventions/error-handling.md @@ -9,7 +9,7 @@ application/domain outcome → application mapping → HttpExceptionFilter Сохраняй принятый профиль RFC 9457: `status`, стабильный публичный `code`, для validation — `errors: [{ pointer, detail }]`. Точный контракт и известное -расхождение validation описаны в [HTTP API](../../../docs/architecture/http-api.md). +расхождение validation описаны в [HTTP API](http-api.md#источник-контракта). Не добавляй `type/title/instance` как обязательные поля без contract change. Чистый domain не импортирует Nest HTTP exceptions. Существующий application @@ -21,7 +21,7 @@ error framework/registry. Если domain outcome требует HTTP interpreta - Используй existing code владельца, не сравнивай внутренние сообщения. Новое значение имеет один symbolic owner; metadata/validation и consumers - согласуются по [domain values](../../../.ai/conventions/code-health.md#domain-values). + согласуются по [domain values](engineering.md#domain-values). - Общий envelope, default status mapping и unexpected errors принадлежат [HttpExceptionFilter](../../src/common/filters/http-exception.filter.ts). Не дублируй `res.status().json()` в controllers/services. diff --git a/.ai/conventions/http-api.md b/.ai/conventions/http-api.md index 81e3353..7e24e75 100644 --- a/.ai/conventions/http-api.md +++ b/.ai/conventions/http-api.md @@ -3,8 +3,8 @@ ## Выбор boundary Сначала определи, меняется wire contract или только реализация существующего -use case. Для route/request/response/status/error shape читай -[HTTP semantics и источник контракта](../../../docs/architecture/http-api.md). +use case. Для route/request/response/status/error shape используй +[локальный публичный контракт](#источник-контракта) и configured HTTP tests. | Проверка | Где выполнять | | --- | --- | @@ -57,11 +57,32 @@ helper ради первого endpoint. Новая pagination явно опре [Operations controller](../../src/modules/investments/operations/presentation/brokerage-operations.controller.ts) показывает guard, user.sub, DTO query и service delegation. -Для нового field обновляй canonical source и generated artifacts в определённом -направлении; body type frontend не исправляется вручную. +Для нового field обновляй Nest DTO/metadata и публикуемый OpenAPI artifact. +Потребители получают публичный контракт независимо от внутреннего устройства API. Проверь happy path и применимые invalid input/access/not-found/domain/upstream errors на boundary, где они возникают. Unit вызов controller не запускает pipes, guards или serialization. `npm run test:openapi-contract` проверяет schema parity, но не поведение invalid request; выбирай соответствующий test по [Testing](testing.md). Финальный gate не заменяет semantic review ошибок. + +## Источник контракта + +Nest DTO/controllers/Swagger metadata определяют публичный HTTP API. +`npm run openapi:generate` собирает backend и записывает корневой `openapi.json`. +Nest preview читает metadata без запуска HTTP-порта, провайдеров, БД и T-Bank; +команда не требует deployment environment. Artifact исключён из Git. + +При изменении API обнови DTO/metadata и выполни генерацию. Потребители получают +artifact отдельно; backend не читает их репозитории. `test:openapi-contract` +сравнивает вывод генератора с runtime schema configured app во временном каталоге. +Проверка подтверждает правильность генерации, но без versioned baseline не +выявляет breaking change относительно предыдущего выпуска. Для такого изменения +согласуй переход consumers и проверь relevant HTTP behavior. + +HTTP prefix — `/api/v1`; Swagger доступен согласно `src/config/swagger.config.ts`. +Общий error response — `application/problem+json` со `status` и публичным `code`. +Validation требует `400 / VALIDATION_ERROR` и `errors: [{ pointer, detail }]`. +Известное ограничение: ValidationPipe сейчас формирует строковые сообщения, +а filter ожидает структурированные field errors; schema parity не доказывает +правильность `errors`. Изменение требует проверки реального HTTP pipeline. diff --git a/.ai/conventions/integrations.md b/.ai/conventions/integrations.md index ebc2951..d742ab0 100644 --- a/.ai/conventions/integrations.md +++ b/.ai/conventions/integrations.md @@ -5,10 +5,9 @@ T-Bank gRPC принадлежит TbankClient; MOEX metadata — MoexClient; NCR ratings — NcrRatingsClient. Сервисы use case используют объявленные клиентом records; SDK clients, metadata и transport errors -остаются внутри integration. Frontend не вызывает -T-Bank и не получает credentials/proto client. Reference source — -[ADR 003](../../../docs/adr/003-tbank-server-side-grpc-boundary.md) и -[connection behavior](../../../docs/integrations/tbank.md). +остаются внутри integration. Credentials и proto client не входят в публичный +HTTP API. Connection lifecycle проверяется на границе соответствующего module +и [Security](security-auth.md). `modules/investments/tbank-connection` — пользовательская capability чтения, создания/замены и удаления подключения. `integrations/tbank` — технические @@ -39,7 +38,8 @@ Existing client уже владеет bounded FIFO queue, rate limit, deadline классификацией. Новый RPC проходит тот же путь; не добавляй вложенный retry loop в service. Не отключай TLS verification ради окружения: используй документированный CA mechanism. Точные параметры и ограничения принадлежат -[connection configuration](../../../docs/integrations/tbank.md#конфигурация-и-ограничения). +[validated environment](../../src/config/env.validation.ts) и +[TbankClient](../../src/integrations/tbank/tbank.client.ts). Эта queue/retry policy относится к T-Bank. MOEX/NCR HTTP clients используют собственный bounded timeout и безопасный unavailable outcome; не приписывай им @@ -47,9 +47,10 @@ gRPC retry policy. При изменении upstream сначала прочи При query/cache change установи key, user/account/generation ownership, freshness и поведение late response. Старый результат не может обслужить новую -connection generation. Client Cached Snapshot frontend отличается от backend -cache/stale policy; semantics конкретного use case читай через -[feature index](../../../docs/README.md#предметные-области). +connection generation. Успешная замена/deletion подключения меняет generation +даже при сохранении прежнего token; ошибка оставляет прежнее подключение. +Кэш конкретного use case принадлежит его service и tests. Не переноси +сохранённый snapshot между пользователями или поколениями подключения. Внешний auth failure и временная недоступность имеют разные public codes; raw gRPC exception не уходит клиенту. Existing translation — в TbankTokenService diff --git a/.ai/conventions/observability.md b/.ai/conventions/observability.md index ae028cb..66d56b9 100644 --- a/.ai/conventions/observability.md +++ b/.ai/conventions/observability.md @@ -17,8 +17,6 @@ - Для ошибки используй public code и безопасный context операции/попыток; [error mapping](error-handling.md) не отдаёт внутренние детали клиенту. -Причины — [ADR 008](../../../docs/adr/008-pino-backend-logging.md), системное -поведение — [HTTP observability](../../../docs/architecture/http-api.md#наблюдаемость). Проверяй новые поля/redaction/correlation existing logging tests; не добавляй тест на каждую строку log. Existing ESLint не запрещает все вызовы `console` отдельным правилом: до настройки встроенного `no-console` это semantic review. diff --git a/.ai/conventions/security-auth.md b/.ai/conventions/security-auth.md index f4ecf59..6c571d0 100644 --- a/.ai/conventions/security-auth.md +++ b/.ai/conventions/security-auth.md @@ -10,21 +10,20 @@ account access/connection generation — TbankAccessService; token crypto — TbankTokenService/TbankCrypto. Расширяй существующую boundary, не добавляй второй JWT middleware, decrypt helper или refresh coordinator. -[Authentication](../../../docs/domains/authentication/overview.md) и -[ADR 006](../../../docs/adr/006-auth-session-policy.md)/[009](../../../docs/adr/009-concurrent-refresh-rotation-policy.md) -задают session semantics. Access хранится только в памяти frontend; refresh -передаётся httpOnly cookie. Source code конфигурации cookie — +Access проверяется JWT strategy; refresh передаётся httpOnly cookie. +Сроки token/session принадлежат auth source и configured HTTP tests. +Source code конфигурации cookie — [AuthController](../../src/modules/auth/presentation/auth.controller.ts); изменение flags/path требует проверки browser/HTTP поведения, не только TypeScript. Refresh rotation атомарна: один победитель, проигравший/superseded token получает 401, но сам по себе не отзывает session победителя. Не вводи replay history/grace -или иной security contract без текущей задачи. Logout идемпотентен; in-memory -frontend session/cache очищается и при network failure. +или иной security contract без текущей задачи. Logout идемпотентен, отзывает +сессию и очищает refresh cookie на HTTP-границе. ## Trust boundaries -Frontend не является security boundary. DTO runtime validation выполняется +Клиентский интерфейс не является security boundary. DTO runtime validation выполняется server-side; whitelist не заменяет access checks. Environment/secret inputs проверяет existing config owner; реальные credentials не попадают в fixtures. @@ -36,8 +35,10 @@ cases для соответствующего изменения. T-Bank credentials остаются server-side и хранятся encrypted. Используй existing AES-256-GCM boundary с validated 32-byte key; не печатай plaintext, шифротекст, cookie, Authorization или connection generation в logs/errors/API. -[Connection source](../../../docs/integrations/tbank.md) определяет -cache invalidation при замене/deletion и ограничение key rotation. +Успешная замена/deletion меняет connection generation и инвалидирует её кэши; +ошибка сохраняет прежнее подключение. Ключ не ротируется автоматически: +новый ключ не расшифрует сохранённые credentials. Нужен исходный ключ или +повторное сохранение token пользователем; сохраняй ключ отдельно от backup БД. Не передавай Prisma User/Session целиком наружу; выбирай public DTO. Пароль обрабатывает существующий bcrypt flow. Не копируй требования о нормализации @@ -56,5 +57,5 @@ cache invalidation при замене/deletion и ограничение key ro message безопасным. Targeted auth/access/crypto/logging tests + [Testing](testing.md). -Cookie/guard/HTTP changes требуют configured HTTP proof; frontend Bearer/retry -проверяется MSW. Schema parity не доказывает отсутствие утечки secret. +Cookie/guard/HTTP changes требуют configured HTTP proof. +Schema parity не доказывает отсутствие утечки secret. diff --git a/.ai/conventions/testing.md b/.ai/conventions/testing.md index 0b64f93..69dd122 100644 --- a/.ai/conventions/testing.md +++ b/.ai/conventions/testing.md @@ -1,7 +1,7 @@ # Backend testing Перед нетривиальными тестами сформулируй -[общий Test Intent](../../../.ai/conventions/testing.md); отдельный документ +[общий Test Intent](engineering.md#test-intent); отдельный документ не требуется. Trivial low-risk изменения обходятся без него. ## Выбор уровня @@ -15,7 +15,7 @@ | Pure mapper/decimal/domain branch | Unit со значимыми edge cases | | Service orchestration/access/retry policy | Jest с mocks внешних boundaries и observable outcome | | Pipe/guard/cookie/status/HTTP error | Configured Nest HTTP boundary; прямого вызова метода controller недостаточно | -| OpenAPI metadata/wire schema | `test:openapi-contract` + потребительский contract check | +| OpenAPI metadata/wire schema | `test:openapi-contract` + configured HTTP tests | | DB constraint/CAS/transaction/migration | Integration с изолированной реальной DB | | Только DTO declarations/thin wiring | Existing contract/type/build без искусственного unit test | @@ -29,7 +29,7 @@ Unit specs colocated в `src/**/*.spec.ts`; HTTP/integration — `test/`. - [BrokerageAccountsService tests](../../src/modules/investments/brokerage-accounts/brokerage-accounts.service.spec.ts): отсутствие connection не вызывает upstream, нормализация и точные суммы. - [AuthService tests](../../src/modules/auth/application/auth.service.spec.ts): invalid/expired/superseded token и concurrent rotation; mock race test не является доказательством DB transaction. - [HTTP filter tests](../../src/common/filters/http-exception.filter.spec.ts): безопасный error envelope. -- [OpenAPI integration](../../test/openapi-contract.e2e-spec.ts): configured app + frontend snapshot, без записи snapshot и без T-Bank. +- [OpenAPI integration](../../test/openapi-contract.e2e-spec.ts): генератор без сервера + configured app; временный JSON, без versioned snapshot и без T-Bank. При bug сначала воспроизведи именно его исход, затем исправь responsible owner. Не создавай tests для каждой декларации, logs строки или поведения Prisma/Nest diff --git a/.ci/refs.json b/.ci/refs.json deleted file mode 100644 index a25a11c..0000000 --- a/.ci/refs.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "frontend": "0244acf429c525b23117f165bbe52ef5403eefd3" -} diff --git a/.gitea/workflows/verify.yml b/.gitea/workflows/verify.yml index 245e3fd..7878842 100644 --- a/.gitea/workflows/verify.yml +++ b/.gitea/workflows/verify.yml @@ -8,39 +8,13 @@ on: jobs: verify: runs-on: ubuntu-latest - defaults: - run: - working-directory: backend steps: - uses: https://github.com/actions/checkout@v4 - with: - path: backend - - name: Checkout compatible peer revisions - env: - SOURCE_BRANCH: ${{ github.head_ref || github.ref_name }} - TARGET_BRANCH: ${{ github.base_ref || github.ref_name }} - run: | - set -eu - for repo in frontend; do - ref=develop - case "$SOURCE_BRANCH:$TARGET_BRANCH" in - release/*:*|*:main) - ref=$(jq -er --arg repo "$repo" '.[$repo]' .ci/refs.json) - printf '%s\n' "$ref" | grep -Eq '^[0-9a-f]{40}$' - ;; - esac - target="../$repo" - if [ "$repo" = main ]; then target=..; fi - git init "$target" - git -C "$target" remote add origin "https://git.personal.ksv741.ru/WealthLedger/$repo.git" - git -C "$target" fetch --depth 1 origin "$ref" - git -C "$target" checkout --detach FETCH_HEAD - git -C "$target" rev-parse HEAD - done - git rev-parse HEAD + - name: Record checked revision + run: git rev-parse HEAD - uses: https://github.com/actions/setup-node@v4 with: - node-version-file: backend/.nvmrc + node-version-file: .nvmrc - run: npm ci - name: Generate Prisma client before typechecks run: npx --no-install prisma generate diff --git a/.gitignore b/.gitignore index bd5b20f..c01f41d 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,6 @@ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json /dev.db-* /prisma/*.db /prisma/*.db-* + +# Generated public API artifact +/openapi.json diff --git a/AGENTS.md b/AGENTS.md index 2c50351..9d0a188 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,8 +1,9 @@ # Backend router -Общие инструкции — [workspace AGENTS](../AGENTS.md). Объединяй triggers задачи, -путей и обнаруженных boundaries; читай только нужные темы до решения. -Системные факты/spec/ADR выбирай в разделе [Источники](#источники) по вопросу. +Репозиторий самостоятельный: обязательные инструкции и проверки используют +только его файлы и предоставленные публичные контракты. Перед работой прочитай +[порядок работы](.ai/conventions/engineering.md#граница-и-порядок-работы), затем +объединяй triggers задачи и путей; читай активированные темы до решения. | Решение / сигнал | Читать | | --- | --- | @@ -15,28 +16,23 @@ | JWT/cookie/session/guard/access, encryption, secrets | [Security](.ai/conventions/security-auth.md) | | External client/gateway/SDK/proto, retry/rate limit/cache | [Integrations](.ai/conventions/integrations.md) | | Logging/context/levels, sensitive logs | [Observability](.ai/conventions/observability.md); sensitive data также Security | -| Изменение публичного wire contract / Swagger metadata | HTTP API + [HTTP contract source](../docs/architecture/http-api.md#источник-контракта) | -| Helper/dependency/новый pattern, refactor/scope | [Code health](../.ai/conventions/code-health.md), для пакета — [Dependencies](../.ai/conventions/dependencies.md) | -| Выбор gate, tooling, завершение | [Guardrails](../.ai/conventions/guardrails.md) | +| Изменение публичного wire contract / Swagger metadata | [HTTP API](.ai/conventions/http-api.md#источник-контракта) | +| Helper/dependency/новый pattern, refactor/scope | [Change scope](.ai/conventions/engineering.md#change-scope), для пакета — [Dependencies](.ai/conventions/engineering.md#dependencies) | +| Выбор gate, tooling, завершение | [Проверки](.ai/conventions/engineering.md#проверки-и-завершение) | Новый endpoint обычно требует HTTP API + Testing; Data/Security добавляются по persistence/access trigger. Auth refresh активирует Security + Data + Testing. -Повтори routing по итоговому diff, включая новые файлы. Migration backlog -читается только при соответствующем finding через -[plan](../.ai/migrations/conventions-v1/plan.md). +Перед завершением повтори routing по новым и изменённым файлам. Для Git/release +прочитай [Git и релизы](.ai/conventions/engineering.md#git-и-релизы). ## Источники -Открывай только источник по возникшему вопросу. Code показывает реализацию, -docs — текущую систему, ADR — rationale, specs — requirements. Старый пример -не отменяет контракт; conflicting security/data facts выясняй до решения. - -| Вопрос | Источник | +| Вопрос | Локальный источник | | --- | --- | -| HTTP errors/DTO/wire shape | [HTTP API](../docs/architecture/http-api.md); editable OpenAPI генерирует Nest | -| Auth/session, account access, secrets | [Authentication](../docs/domains/authentication/overview.md), [T-Bank](../docs/integrations/tbank.md) | -| Prisma/data ownership, хранение и доступ | [Containers](../docs/architecture/containers.md#хранимые-данные-и-границы-доступа) | -| T-Bank/cache/logging | [T-Bank](../docs/integrations/tbank.md), [Freshness](../docs/architecture/cross-cutting/data-freshness.md), [Observability](../docs/architecture/http-api.md#наблюдаемость) | -| Money, account/position/income/cache meanings | [Глоссарий](../docs/glossary.md) | -| Поведение области / требования | [Области docs](../docs/README.md#предметные-области), [Specs index](../specs/AI_INDEX.md) | +| HTTP errors/DTO/wire shape | [HTTP API](.ai/conventions/http-api.md), генерация `npm run openapi:generate`, configured HTTP tests | +| Auth/session, account access, secrets | [Security](.ai/conventions/security-auth.md) и auth/access tests | +| Prisma/data ownership | [Data](.ai/conventions/data.md), Prisma schema/migrations | +| T-Bank/cache/logging | [Integrations](.ai/conventions/integrations.md), [Observability](.ai/conventions/observability.md) | +| Money и финансовые расчёты | [Data](.ai/conventions/data.md#деньги-и-точность), domain source и tests владельца | +| Поведение области | Module source и tests; неясное ожидание уточни до изменения | | Versions/scripts/check scope | Manifest/lockfile, ESLint/TypeScript/Jest/Prisma configs | diff --git a/README.md b/README.md index 1d5d6a3..bb819cd 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # WealthLedger backend NestJS API WealthLedger. Backend владеет auth, SQLite-данными и server-side -интеграцией T-Bank; frontend не обращается к T-Bank напрямую. +интеграциями. Потребители используют публичный HTTP API и OpenAPI artifact; +их репозитории не нужны для разработки, сборки и проверок backend. ## Быстрый старт @@ -33,7 +34,7 @@ npm run verify application/test typechecks, Jest и build. `npm run lint:fix` — отдельная записывающая команда, `npm run test:cov` — диагностика без общего числового threshold. `npm run test:e2e` отдельно проверяет все HTTP suites на новых базах -и runtime OpenAPI с соседним frontend checkout. Для полной backend-проверки +и соответствие генерируемого OpenAPI настроенному приложению. Для полной backend-проверки выполните обе команды. Nest 12 поставляется как ESM. Скрипты Jest включают `--experimental-vm-modules`, @@ -44,11 +45,9 @@ TypeScript 7 требует поддержки со стороны `ts-jest` и ### CI и обновления зависимостей `.gitea/workflows/verify.yml` запускает `npm ci`, генерацию Prisma client, -`npm run verify` и `npm run test:e2e` на push и PR. Frontend snapshot для -разработки берётся из `develop`; для `main`, PR в `main` и `release/*` — из -полного SHA в `.ci/refs.json`. Лог сохраняет оба SHA. Изменение -контракта согласуется с этой frontend revision. Успешный unit gate не заменяет -SQLite/e2e; ошибка любой suite завершает job с FAIL. +`npm run verify` и `npm run test:e2e` на push и PR. Используется только checkout +backend и его зависимости; OpenAPI генерируется во время проверки, SHA записывается в лог. +Успешный unit gate не заменяет SQLite/e2e; ошибка любой suite завершает job с FAIL. `security.yml` сканирует всю Git-историю Gitleaks 8.30.1 с полностью скрытыми значениями и выполняет `npm audit --include=dev --audit-level=low` при push, @@ -83,20 +82,34 @@ runtime-валидацию. В обычном коде нужны инициал - [HTTP API](.ai/conventions/http-api.md), [data](.ai/conventions/data.md) и [testing](.ai/conventions/testing.md) - [Security/auth](.ai/conventions/security-auth.md) и [integrations](.ai/conventions/integrations.md) -- [Карта проектных фактов](../docs/README.md) -- [API contract и runtime OpenAPI](../docs/architecture/http-api.md#источник-контракта) -- [Обзор поставленной системы](../docs/overview.md) -- [Архитектурные решения](../docs/adr/README.md) +- [Генерация OpenAPI](#генерация-openapi) -Из backend runtime OpenAPI можно read-only сверить с frontend snapshot: +Генерация без сервера сверяется с OpenAPI настроенного приложения: ```bash npm run test:openapi-contract ``` -Разработка идёт напрямую по задаче пользователя. Каждый affected repo проходит -свой verify; при изменении wire contract дополнительно выполняется contract check. -Обязательного OpenSpec/SDD lifecycle нет. +## Генерация OpenAPI + +```bash +nvm use --silent +npm run openapi:generate +``` + +Команда собирает backend и записывает `openapi.json` в корень репозитория. +Сервер и HTTP-порт не запускаются; Nest preview читает metadata без создания +провайдеров и подключения к БД/T-Bank. Настроенный `.env` и реальные секреты +не нужны. `openapi.json` исключён из Git и каждый раз создаётся из DTO/Swagger +metadata; передай полученный artifact потребителю отдельно. + +`npm run test:openapi-contract` собирает backend и сравнивает результат генератора +с runtime schema настроенного приложения, используя временный каталог и SQLite. +`npm run test:e2e` также сначала собирает backend. Изменение публичного API +проверяй relevant HTTP tests: без versioned snapshot сравнение не отслеживает +breaking changes относительно предыдущего выпуска. + +Разработка идёт напрямую по задаче пользователя; OpenSpec/SDD — по явному запросу. ## Реальная SQLite и сохранность данных @@ -161,4 +174,38 @@ npm run test:durability -- "$baseline_dir" Durability оставляет временные DB, логи и `result.json` по пути из вывода для диагностики; он не откатывает schema пользовательской базы. -Версии и ветки: [процесс релиза](../README.md#релизы-и-ветки). +## Релизы + +Репозиторий выпускается независимо. `develop` — интеграция, `main` — принятые +релизы. Работа: `codex/` от актуального `develop`, PR в `develop`. +Обычные PR допускают squash; release и обратная синхронизация сохраняют историю. +Commits и PR titles — Conventional Commits, включая `spec:`. Версии вручную не повышай. + +1. Получи `main`, `develop` и теги: `git fetch origin --tags`. Убедись, что + `develop` содержит последний релиз. Создай `release/` от проверенного + `develop`; `nvm use --silent`, `npm ci`. Hotfix начинается от `main` в + `release/hotfix-` и проходит тот же процесс. +2. На чистой ветке выполни `npm run release:preview`, просмотри версию/changelog, + затем `npm run release:prepare`. Команда создаёт `chore(release): X.Y.Z`, + обновляет manifest/lockfile/CHANGELOG; тег и push не выполняет. Не обходи + guards ветки, чистого дерева, отсутствующих тегов или уже подготовленной версии. +3. Проверь diff и штатные gates этого репозитория. Отправь только release-ветку, + открой PR в `main` с заголовком `chore(release): X.Y.Z`. После зелёных + `verify`, `security`, `commits` выполни fast-forward-only merge. Если `main` + изменился, согласуй историю и повтори проверки кандидата. +4. Получи удалённый `main`, сравни его HEAD с проверенным release SHA и версией + manifest/lockfile. Создай аннотированный `vX.Y.Z` именно на этом SHA: + `git tag -a vX.Y.Z -m "Release X.Y.Z"`, затем отправь только + `git push origin refs/tags/vX.Y.Z`. Не запускай release-it повторно на `main`. +5. Верни release-коммит через PR `main → develop` без squash/rebase; проверь, + что `develop` содержит тег. Опубликованные теги не перемещай. При сбое после + merge создай отсутствующий тег без нового bump; до merge исправь и проверь + существующий кандидат без повторного повышения версии. + +`feat:` повышает minor, `fix:`/`perf:` и прочие разрешённые типы — patch; +`!`/`BREAKING CHANGE:` — major, включая версии до 1.0. `chore(release):` +исключён из рекомендации и changelog. Конфигурация — `.release-it.json`. +В Gitea для `main`/`develop` должны быть запрещены прямой/force push и обход +администратором, обязательны `verify`, `security`, `commits`; теги `v*` защищены. +Workflow сам не включает эти настройки. Отдельное review approval для одного +владельца не требуется. Npm publishing, deploy и Gitea Releases не включены. diff --git a/package.json b/package.json index b227fd9..1458b1e 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "scripts": { "build": "prisma generate && nest build", + "openapi:generate": "npm run build && node scripts/generate-openapi.cjs", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest start", "start:dev": "NODE_ENV=development nest start --watch --path tsconfig.dev.json", @@ -24,10 +25,10 @@ "test:watch": "npm test -- --watch", "test:cov": "npm test -- --coverage", "test:debug": "node --experimental-vm-modules --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "node test/sqlite.cjs --all", + "test:e2e": "npm run build && node test/sqlite.cjs --all", "test:sqlite": "node test/sqlite.cjs", "test:durability": "node test/bank-cards-durability.cjs", - "test:openapi-contract": "node test/sqlite.cjs openapi-contract.e2e-spec.ts", + "test:openapi-contract": "npm run build && node test/sqlite.cjs openapi-contract.e2e-spec.ts", "verify": "npm run lint && npm run typecheck:app && npm run typecheck:test && npm test -- --runInBand && npm run build", "release:preview": "release-it --dry-run --ci", "release:prepare": "release-it --ci", diff --git a/scripts/generate-openapi.cjs b/scripts/generate-openapi.cjs new file mode 100644 index 0000000..3845d46 --- /dev/null +++ b/scripts/generate-openapi.cjs @@ -0,0 +1,40 @@ +const { randomBytes } = require('node:crypto'); +const { writeFile } = require('node:fs/promises'); + +// Schema generation needs no deployment configuration or real credentials. +Object.assign(process.env, { + DATABASE_URL: 'file::memory:', + FRONTEND_ORIGIN: 'http://localhost', + JWT_SECRET: randomBytes(32).toString('hex'), + TOKEN_ENCRYPTION_KEY: randomBytes(32).toString('base64'), + PORT: '3000', + LOG_LEVEL: 'error', + TBANK_GRPC_ENDPOINT: 'localhost:443', + TBANK_RATE_LIMIT_PER_SECOND: '5', + TBANK_CA_CERT_PATH: '', +}); + +const { NestFactory } = require('@nestjs/core'); +const { AppModule } = require('../dist/src/app.module.js'); +const { API_PREFIX, createOpenApiDocument } = require('../dist/src/openapi.js'); + +async function generate() { + const app = await NestFactory.create(AppModule, { + preview: true, + logger: false, + }); + try { + app.setGlobalPrefix(API_PREFIX); + await writeFile( + 'openapi.json', + JSON.stringify(createOpenApiDocument(app), null, 2) + '\n', + ); + } finally { + await app.close(); + } +} + +generate().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/src/main.ts b/src/main.ts index fbe3dba..e80447a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,7 +2,8 @@ import { INestApplication, ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import cookieParser from 'cookie-parser'; import { ConfigService } from '@nestjs/config'; -import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { SwaggerModule } from '@nestjs/swagger'; +import { API_PREFIX, createOpenApiDocument } from './openapi'; import { Logger } from 'nestjs-pino'; import { AppModule } from './app.module'; import { HttpExceptionFilter } from './common/filters/http-exception.filter'; @@ -21,7 +22,7 @@ export function configureApp(app: INestApplication): void { credentials: true, exposedHeaders: ['X-Request-ID'], }); - app.setGlobalPrefix('api/v1'); + app.setGlobalPrefix(API_PREFIX); app.use(cookieParser()); const requestLogging = app.get(RequestLoggingMiddleware); app.use(requestLogging.use.bind(requestLogging)); @@ -36,13 +37,7 @@ async function bootstrap() { const config = app.get>(ConfigService); if (shouldExposeSwagger(process.env['NODE_ENV'])) { - const document = SwaggerModule.createDocument( - app, - new DocumentBuilder() - .setTitle('WealthLedger API') - .setVersion('v1') - .build(), - ); + const document = createOpenApiDocument(app); SwaggerModule.setup('api/docs', app, document); } diff --git a/src/openapi.ts b/src/openapi.ts new file mode 100644 index 0000000..46e13fb --- /dev/null +++ b/src/openapi.ts @@ -0,0 +1,11 @@ +import { INestApplication } from '@nestjs/common'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; + +export const API_PREFIX = 'api/v1'; + +export function createOpenApiDocument(app: INestApplication) { + return SwaggerModule.createDocument( + app, + new DocumentBuilder().setTitle('WealthLedger API').setVersion('v1').build(), + ); +} diff --git a/test/openapi-contract.e2e-spec.ts b/test/openapi-contract.e2e-spec.ts index c225252..a201c2c 100644 --- a/test/openapi-contract.e2e-spec.ts +++ b/test/openapi-contract.e2e-spec.ts @@ -1,22 +1,12 @@ import { INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; -import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; -import { readFile } from 'node:fs/promises'; +import { execFileSync } from 'node:child_process'; +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; import { resolve } from 'node:path'; import { AppModule } from '../src/app.module'; import { configureApp } from '../src/main'; - -function canonicalize(value: unknown): unknown { - if (Array.isArray(value)) return value.map(canonicalize); - if (value && typeof value === 'object') { - return Object.fromEntries( - Object.entries(value) - .sort(([left], [right]) => left.localeCompare(right)) - .map(([key, nestedValue]) => [key, canonicalize(nestedValue)]), - ); - } - return value; -} +import { createOpenApiDocument } from '../src/openapi'; describe('OpenAPI contract (integration)', () => { let app: INestApplication; @@ -32,21 +22,38 @@ describe('OpenAPI contract (integration)', () => { afterAll(() => app.close()); - it('matches the committed frontend OpenAPI snapshot', async () => { - const runtimeDocument = SwaggerModule.createDocument( - app, - new DocumentBuilder() - .setTitle('WealthLedger API') - .setVersion('v1') - .build(), - ); - const snapshot = JSON.parse( - await readFile( - resolve(__dirname, '../../frontend/src/shared/api/spec/openapi.json'), - 'utf8', - ), - ) as unknown; - - expect(canonicalize(runtimeDocument)).toEqual(canonicalize(snapshot)); + it('generates the runtime contract without server startup or deployment configuration', async () => { + const directory = await mkdtemp(resolve(tmpdir(), 'wealthledger-openapi-')); + try { + const networkGuard = resolve(directory, 'no-network.cjs'); + await writeFile( + networkGuard, + ` + const net = require('node:net'); + const forbidden = () => { throw new Error('Schema generation must not use the network'); }; + net.Server.prototype.listen = forbidden; + net.Socket.prototype.connect = forbidden; + `, + ); + execFileSync( + process.execPath, + [ + '--require', + networkGuard, + resolve(__dirname, '../scripts/generate-openapi.cjs'), + ], + { + cwd: directory, + env: { PATH: process.env.PATH }, + timeout: 30_000, + }, + ); + const generated = JSON.parse( + await readFile(resolve(directory, 'openapi.json'), 'utf8'), + ) as unknown; + expect(generated).toEqual(createOpenApiDocument(app)); + } finally { + await rm(directory, { recursive: true, force: true }); + } }); }); -- 2.54.0 From 1a6be432f24f757728a03415051392069112af1f Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Wed, 16 Sep 2026 09:41:56 +0300 Subject: [PATCH 4/6] feat: add backend container delivery --- .dockerignore | 13 +++ .gitea/workflows/image.yml | 59 +++++++++++ Dockerfile | 41 ++++++++ README.md | 27 +++++ package-lock.json | 130 +------------------------ package.json | 3 +- src/integrations/tbank/tbank.client.ts | 10 +- test/container-smoke.cjs | 69 +++++++++++++ 8 files changed, 214 insertions(+), 138 deletions(-) create mode 100644 .dockerignore create mode 100644 .gitea/workflows/image.yml create mode 100644 Dockerfile create mode 100644 test/container-smoke.cjs diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..479a240 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +** +!package.json +!package-lock.json +!.nvmrc +!nest-cli.json +!tsconfig*.json +!prisma.config.ts +!prisma/ +!prisma/schema.prisma +!prisma/migrations/ +!prisma/migrations/** +!src/ +!src/** diff --git a/.gitea/workflows/image.yml b/.gitea/workflows/image.yml new file mode 100644 index 0000000..5979547 --- /dev/null +++ b/.gitea/workflows/image.yml @@ -0,0 +1,59 @@ +name: Backend image + +on: + push: + tags: ['v*'] + +concurrency: + group: backend-image-publication + cancel-in-progress: false + +jobs: + publish: + runs-on: ubuntu-latest + env: + IMAGE: git.personal.ksv741.ru/wealthledger/backend + RELEASE_TAG: ${{ gitea.ref_name }} + REVISION: ${{ gitea.sha }} + DOCKER_CONFIG: ${{ runner.temp }}/wealthledger-backend-docker + steps: + - uses: https://github.com/actions/checkout@v4 + - uses: https://github.com/actions/setup-node@v4 + with: + node-version-file: .nvmrc + - name: Validate release version + run: | + node --input-type=commonjs -e 'const p = require("./package.json"); const l = require("./package-lock.json"); if (!/^v\d+\.\d+\.\d+$/.test(process.env.RELEASE_TAG) || process.env.RELEASE_TAG !== `v${p.version}` || l.version !== p.version || l.packages[""].version !== p.version) process.exit(1)' + test "$(git rev-parse HEAD)" = "$REVISION" + docker version + docker buildx version + - run: npm ci + - run: npx --no-install prisma generate + - run: npm run verify + - run: npm run test:e2e + - name: Login with dedicated package writer + env: + REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + run: | + test -n "$REGISTRY_USERNAME" && test -n "$REGISTRY_TOKEN" + install -d -m 700 "$DOCKER_CONFIG" + printf '%s' "$REGISTRY_TOKEN" | docker login git.personal.ksv741.ru --username "$REGISTRY_USERNAME" --password-stdin + - name: Publish immutable release tag + run: | + VERSION="${RELEASE_TAG#v}" + if docker manifest inspect "$IMAGE:$VERSION" >/dev/null 2>manifest-error.log; then + echo 'Refusing to overwrite an existing release tag'; exit 1 + fi + if ! grep -Eq 'manifest unknown|no such manifest' manifest-error.log; then + echo 'Cannot prove tag absence; refusing publication'; exit 1 + fi + docker buildx build --target production --build-arg VERSION="$VERSION" --build-arg REVISION="$REVISION" --tag "$IMAGE:$VERSION" --metadata-file image-metadata.json --push . + DIGEST="$(node --input-type=commonjs -p 'require("./image-metadata.json")["containerimage.digest"]')" + docker pull "$IMAGE@$DIGEST" + test "$(docker image inspect "$IMAGE@$DIGEST" --format '{{index .Config.Labels "org.opencontainers.image.revision"}}')" = "$REVISION" + npm run test:container -- "$IMAGE@$DIGEST" + printf 'Backend image: %s@%s\n' "$IMAGE" "$DIGEST" + - name: Remove registry session + if: always() + run: rm -rf "$DOCKER_CONFIG" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..56f6250 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,41 @@ +FROM node:24.21.0-bookworm-slim AS base +WORKDIR /app + +FROM base AS dependencies +# Native SQLite/bcrypt builds happen only here; compilers stay out of runtime. +RUN apt-get update && apt-get install -y --no-install-recommends python3=3.11.2-1+b1 make=4.3-4.1 g++=4:12.2.0-3 ca-certificates=20250419~deb12u1 openssl=3.0.20-1~deb12u2 && rm -rf /var/lib/apt/lists/* +COPY package.json package-lock.json ./ +RUN npm ci +COPY . . +RUN npx --no-install prisma generate + +FROM dependencies AS development +# Nest watch uses ps to stop the previous process tree before restarting. +RUN apt-get update && apt-get install -y --no-install-recommends procps=2:4.0.2-3 && rm -rf /var/lib/apt/lists/* +RUN mkdir /data && chown node:node /data && chown -R node:node /app +USER node +ENV NODE_ENV=development DATABASE_URL=file:/data/wealthledger.db +EXPOSE 3000 +CMD ["npm", "run", "start:dev"] + +FROM dependencies AS build +RUN npm run build && npm prune --omit=dev --omit=optional + +FROM base AS production +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates=20250419~deb12u1 openssl=3.0.20-1~deb12u2 && rm -rf /var/lib/apt/lists/* \ + && mkdir /data && chown node:node /data +ARG VERSION +ARG REVISION +LABEL org.opencontainers.image.version=$VERSION \ + org.opencontainers.image.revision=$REVISION +COPY --from=build /app/node_modules ./node_modules +COPY --from=build /app/dist ./dist +COPY --from=build /app/prisma ./prisma +COPY --from=build /app/prisma.config.ts ./prisma.config.ts +COPY --from=build /app/package.json /app/package-lock.json ./ +ENV NODE_ENV=production DATABASE_URL=file:/data/wealthledger.db +USER node +EXPOSE 3000 +CMD ["node", "dist/src/main.js"] +HEALTHCHECK --interval=10s --timeout=3s --start-period=20s --retries=6 \ + CMD node -e 'fetch("http://127.0.0.1:3000/api/v1/auth/me").then(async r=>{await r.arrayBuffer();process.exit(r.status===401?0:1)}).catch(()=>process.exit(1))' diff --git a/README.md b/README.md index bb819cd..dc94750 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,33 @@ npm run start:dev T-Bank token в `.env`: он вводится пользователем и хранится в зашифрованном виде. +## Контейнерный image + +`Dockerfile` имеет targets `development` (Nest watch, generated Prisma client и Linux native modules) +и `production` (compiled Node process от UID 1000, Prisma CLI/migrations и T-Bank proto assets). Node соответствует `.nvmrc`. +Build context ограничен `.dockerignore`; `.env`, local DB и host node_modules +не попадают в image. + +```bash +docker build --target development -t wealthledger-backend:development . +docker build --target production -t wealthledger-backend:production . +nvm use --silent +npm run test:container -- wealthledger-backend:production +``` + +Gitea `image.yml` запускается на защищённом release tag `vX.Y.Z`, проверяет +package/lock version и canonical gates. Runner `ubuntu-latest` должен иметь +Docker/buildx. Dedicated package writer задаётся Actions secrets +`REGISTRY_USERNAME`/`REGISTRY_TOKEN`; server использует отдельную read-only +identity. Публикуются semver tags, без `latest`; существующие +release tags не перезаписываются. Production выбирает опубликованный version tag. + +Compose topology принадлежит workspace, эксплуатационная последовательность — +[документация](https://git.personal.ksv741.ru/WealthLedger/docs), +страница `architecture/deployment.md`. +Проверка публикации требует настроенного registry/runner; локальный build +не доказывает package permissions. + ## Проверка Единый completion gate не изменяет versioned-файлы: diff --git a/package-lock.json b/package-lock.json index 25d9096..72c6912 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "passport": "0.7.0", "passport-jwt": "4.0.1", "pino-http": "11.0.0", + "prisma": "7.10.0", "reflect-metadata": "0.2.2", "rxjs": "7.8.2" }, @@ -57,7 +58,6 @@ "globals": "17.12.0", "jest": "30.5.1", "prettier": "3.9.6", - "prisma": "7.10.0", "release-it": "21.0.2", "source-map-support": "0.5.21", "supertest": "7.2.2", @@ -1280,14 +1280,12 @@ "version": "0.4.3", "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.3.tgz", "integrity": "sha512-ichuWTgtd4mOM1G4SpyGJa5trT03lWbMypDV0fUXUCXg5hiHqVAz/bZyV68NqmkLB7WcYmj1RMJVSp8HV/v/ZQ==", - "devOptional": true, "license": "Apache-2.0" }, "node_modules/@electric-sql/pglite-socket": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.1.3.tgz", "integrity": "sha512-LAciWM0M1dCL8hlsxu2venbVZcdxema0BtDfpWYVqr+Y468UADw0pFWidhKw1M8sfJ8rdLT71tjMmnirf/IZRQ==", - "devOptional": true, "license": "Apache-2.0", "bin": { "pglite-server": "dist/scripts/server.js" @@ -1300,7 +1298,6 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.3.3.tgz", "integrity": "sha512-AlzLJTRJ8+UFgK8CmxIpyIpJ0+YaFw02IiOSdYrqxwPXdSyeIShz8aa9Tq+tYFXdPwcaMp/Fc80mQZ1dkOQ/wg==", - "devOptional": true, "license": "Apache-2.0", "peerDependencies": { "@electric-sql/pglite": "0.4.3" @@ -3872,7 +3869,6 @@ "version": "7.10.0", "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.10.0.tgz", "integrity": "sha512-Rcg828gIRE3HOQ3pOATFjV5d/P0U9OIobxhd/IMxlfWjA4vru0eGwb0AIwFw0rmcLMVShohZYWPixVxkBHsxUA==", - "devOptional": true, "license": "Apache-2.0", "dependencies": { "c12": "3.3.4", @@ -3891,7 +3887,6 @@ "version": "0.24.17", "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.24.17.tgz", "integrity": "sha512-UvdZzmpFwknnfreh6Jije84ekkYGPYEJhXG1tFzCsCfQyzJifrOo38eZc0qajzvaC6OLUOrN9ML5XfCnEZL9DA==", - "devOptional": true, "license": "ISC", "dependencies": { "@electric-sql/pglite": "0.4.3", @@ -3924,7 +3919,6 @@ "version": "7.10.0", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.10.0.tgz", "integrity": "sha512-KNumN6NHFwybvfdYzTee9pqwx5PvknpWAaHn6L5NsbrKdl+SQrsVZs9opKs6U6SAsvB26HDt3WybRjOhgoWOYQ==", - "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -3938,14 +3932,12 @@ "version": "7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3.tgz", "integrity": "sha512-8OJ6RuZTZ06eFUOtBwxVmv8XMmOW6HWN5F+uxUbZkGxR0Bfab1dfAdXaHPmR5mb59E+fmUo8IOzXlbLY1SClbw==", - "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { "version": "7.10.0", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.10.0.tgz", "integrity": "sha512-0bra1LFYi8xNw0yqV62bHJNQk4BKleOngZiqPWIQc7a3+9q6rqsnqJ15BuepP8943PFMvtmgnP52juZsyYkA6w==", - "devOptional": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "7.10.0" @@ -3955,7 +3947,6 @@ "version": "7.10.0", "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.10.0.tgz", "integrity": "sha512-Zqyu8DY14t6W/xwmAxUYWCXtHrvQnSvT644EAZSsdM8NSmCS74vJJbBKdVsK3ucFpnUWkEpbO1a0CxJXrg130g==", - "devOptional": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "7.10.0", @@ -3967,7 +3958,6 @@ "version": "7.10.0", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.10.0.tgz", "integrity": "sha512-0bra1LFYi8xNw0yqV62bHJNQk4BKleOngZiqPWIQc7a3+9q6rqsnqJ15BuepP8943PFMvtmgnP52juZsyYkA6w==", - "devOptional": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "7.10.0" @@ -3977,7 +3967,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz", "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==", - "devOptional": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "7.2.0" @@ -3987,21 +3976,18 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz", "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==", - "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/query-plan-executor": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz", "integrity": "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==", - "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/streams-local": { "version": "0.1.11", "resolved": "https://registry.npmjs.org/@prisma/streams-local/-/streams-local-0.1.11.tgz", "integrity": "sha512-0TcebL559MByKqTJ+SsrFIEg228iw8UCVRFckzgfRSiJqczhs+MuAgWOF9lnOIV/IVqvu+KMnFTH0eDeTQMpUg==", - "devOptional": true, "license": "Apache-2.0", "dependencies": { "ajv": "^8.12.0", @@ -4018,7 +4004,6 @@ "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "devOptional": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -4035,14 +4020,12 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "devOptional": true, "license": "MIT" }, "node_modules/@prisma/studio-core": { "version": "0.33.0", "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.33.0.tgz", "integrity": "sha512-V2fX/nKEymNTrHXwfP26PGjoLStO35Ogu+ex7CFJbLrMYEcZxxZpiSNOs7px23Hk5mzLWvM5RsqG6Ka+rha+wg==", - "devOptional": true, "license": "Apache-2.0", "dependencies": { "@radix-ui/react-toggle": "1.1.10", @@ -4128,14 +4111,12 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", - "devOptional": true, "license": "MIT" }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -4151,7 +4132,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "devOptional": true, "license": "MIT", "dependencies": { "@radix-ui/react-slot": "1.2.3" @@ -4175,7 +4155,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "devOptional": true, "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" @@ -4194,7 +4173,6 @@ "version": "1.1.10", "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", - "devOptional": true, "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", @@ -4220,7 +4198,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", - "devOptional": true, "license": "MIT", "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", @@ -4240,7 +4217,6 @@ "version": "0.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", - "devOptional": true, "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" @@ -4259,7 +4235,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", - "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -4564,35 +4539,30 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.3.tgz", "integrity": "sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ==", - "devOptional": true, "license": "MIT" }, "node_modules/@types/d3-color": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz", "integrity": "sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==", - "devOptional": true, "license": "MIT" }, "node_modules/@types/d3-delaunay": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz", "integrity": "sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==", - "devOptional": true, "license": "MIT" }, "node_modules/@types/d3-format": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz", "integrity": "sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==", - "devOptional": true, "license": "MIT" }, "node_modules/@types/d3-geo": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", - "devOptional": true, "license": "MIT", "dependencies": { "@types/geojson": "*" @@ -4602,7 +4572,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz", "integrity": "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==", - "devOptional": true, "license": "MIT", "dependencies": { "@types/d3-color": "*" @@ -4612,14 +4581,12 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", - "devOptional": true, "license": "MIT" }, "node_modules/@types/d3-scale": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.2.tgz", "integrity": "sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==", - "devOptional": true, "license": "MIT", "dependencies": { "@types/d3-time": "*" @@ -4629,7 +4596,6 @@ "version": "3.1.7", "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", - "devOptional": true, "license": "MIT", "dependencies": { "@types/d3-path": "*" @@ -4639,14 +4605,12 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz", "integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==", - "devOptional": true, "license": "MIT" }, "node_modules/@types/d3-time-format": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.1.0.tgz", "integrity": "sha512-/myT3I7EwlukNOX2xVdMzb8FRgNzRMpsZddwst9Ld/VFe6LyJyRp0s32l/V9XoUzk+Gqu56F/oGk6507+8BxrA==", - "devOptional": true, "license": "MIT" }, "node_modules/@types/esrecurse": { @@ -4692,7 +4656,6 @@ "version": "7946.0.16", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", - "devOptional": true, "license": "MIT" }, "node_modules/@types/http-errors": { @@ -4761,7 +4724,6 @@ "version": "4.17.25", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.25.tgz", "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==", - "devOptional": true, "license": "MIT" }, "node_modules/@types/methods": { @@ -4843,7 +4805,6 @@ "version": "19.2.18", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", - "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -5560,7 +5521,6 @@ "version": "4.0.1-alpha.0", "resolved": "https://registry.npmjs.org/@visx/curve/-/curve-4.0.1-alpha.0.tgz", "integrity": "sha512-jRu61Uz274pV1zyioXmboyrLutYbnKsgjj4njSGCnhdXj5GkZvZbg+ThDb6oOzoAnJOBRLz4rzPlWvNJOzuVMg==", - "devOptional": true, "license": "MIT", "dependencies": { "@visx/vendor": "4.0.0-alpha.0" @@ -5570,7 +5530,6 @@ "version": "4.0.1-alpha.0", "resolved": "https://registry.npmjs.org/@visx/event/-/event-4.0.1-alpha.0.tgz", "integrity": "sha512-EQqCMSv/s8NbFjo+hz3FKsvvYfP+2QslsFJ/24/O5l/W+7UC6J6aAvO0ujVwrTwdYbuQ+vhxKi1xdPdKR/qj1g==", - "devOptional": true, "license": "MIT", "dependencies": { "@types/react": "*", @@ -5581,7 +5540,6 @@ "version": "4.0.1-alpha.0", "resolved": "https://registry.npmjs.org/@visx/grid/-/grid-4.0.1-alpha.0.tgz", "integrity": "sha512-rycutGmTHO+znNdPumheWMglm7YfpffvRwUkVy5zy4WoORIuKTMkDxwnOzHG2xMxU3EE/YCd37xFV5AxA30yeg==", - "devOptional": true, "license": "MIT", "dependencies": { "@types/react": "*", @@ -5600,7 +5558,6 @@ "version": "4.0.1-alpha.0", "resolved": "https://registry.npmjs.org/@visx/group/-/group-4.0.1-alpha.0.tgz", "integrity": "sha512-V19l7iQ7jccBv8kao/EByuI6o4xtxzzLV9nqVI1hRvmdzTVsuLpqlwzYCZUXJaTVvUWf8s4D2SQFjGkj/Nw+0w==", - "devOptional": true, "license": "MIT", "dependencies": { "@types/react": "*", @@ -5614,14 +5571,12 @@ "version": "4.0.1-alpha.0", "resolved": "https://registry.npmjs.org/@visx/point/-/point-4.0.1-alpha.0.tgz", "integrity": "sha512-ijTfr/Nx09f03vIj9nyTr3z4Xth4Y75427UaogJh6dnIRLMEFHQOwNu791sbfiNj0a+ZXuaE32h0vKrFe4/8Qg==", - "devOptional": true, "license": "MIT" }, "node_modules/@visx/responsive": { "version": "4.0.1-alpha.0", "resolved": "https://registry.npmjs.org/@visx/responsive/-/responsive-4.0.1-alpha.0.tgz", "integrity": "sha512-o+1zGywQZY0+yOx3Iw87wc4bbPJRr/HnIukTwfOz4UVyj9pB1OQNVHB7OORO1+LBHJceWpB31co/ZV9KHncKrA==", - "devOptional": true, "license": "MIT", "dependencies": { "@types/lodash": "^4.17.13", @@ -5636,7 +5591,6 @@ "version": "4.0.1-alpha.0", "resolved": "https://registry.npmjs.org/@visx/scale/-/scale-4.0.1-alpha.0.tgz", "integrity": "sha512-nzjeE87vFSAXGWFiiNfBpNLAf0Q8Qmf6syvKLjqNi4kGZkdhbUll3E/59YsgWXmjM8+llPLWzGsP+JPvo5eq1A==", - "devOptional": true, "license": "MIT", "dependencies": { "@visx/vendor": "4.0.0-alpha.0" @@ -5646,7 +5600,6 @@ "version": "4.0.1-alpha.0", "resolved": "https://registry.npmjs.org/@visx/shape/-/shape-4.0.1-alpha.0.tgz", "integrity": "sha512-62QeiVNmPlterQGwhkEDcbq7M0MqY0lBsK5QKXtM9ZoPZWkuGV3aykA3+Xu20B2FAvyJq4LqJzBc7Sxr+EAdbA==", - "devOptional": true, "license": "MIT", "dependencies": { "@types/lodash": "^4.17.13", @@ -5666,7 +5619,6 @@ "version": "4.0.0-alpha.0", "resolved": "https://registry.npmjs.org/@visx/vendor/-/vendor-4.0.0-alpha.0.tgz", "integrity": "sha512-6I+MuqXBcv9jnlcVowHoHKSdk9gXTWkHLKyqBwRWg7LY6A3Ei8SHfubpqGV5rBUSppxMq2RszPJUS6w+H0YgmQ==", - "devOptional": true, "license": "MIT and ISC", "dependencies": { "@types/d3-array": "3.0.3", @@ -5698,7 +5650,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.1.tgz", "integrity": "sha512-gUY/qeHq/yNqqoCKNq4vtpFLdoCdvyNpWoC/KNjhGbhDuQpAM9sIQQKkXSNpXa9h5KySs/gzm7R88WkUutgwWQ==", - "devOptional": true, "license": "ISC", "dependencies": { "internmap": "1 - 2" @@ -6193,7 +6144,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", - "devOptional": true, "license": "MIT", "engines": { "node": ">= 6.0.0" @@ -6373,7 +6323,6 @@ "version": "2.10.0", "resolved": "https://registry.npmjs.org/better-result/-/better-result-2.10.0.tgz", "integrity": "sha512-oQhh0y1qo2/ZKdAAEvHZAqKKiHOFU5k/bW96fE2ScgQOVkJRiHwB+nOS1SgFsYqRlxMDWvefXi9Q3px7QvgNDw==", - "devOptional": true, "license": "MIT" }, "node_modules/better-sqlite3": { @@ -6595,7 +6544,6 @@ "version": "3.3.4", "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz", "integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==", - "devOptional": true, "license": "MIT", "dependencies": { "chokidar": "^5.0.0", @@ -6624,7 +6572,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", - "devOptional": true, "license": "MIT", "dependencies": { "readdirp": "^5.0.0" @@ -6640,7 +6587,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", - "devOptional": true, "license": "MIT", "engines": { "node": ">= 20.19.0" @@ -6871,7 +6817,6 @@ "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", - "devOptional": true, "license": "MIT" }, "node_modules/cli-cursor": { @@ -7069,7 +7014,6 @@ "version": "0.2.4", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", - "devOptional": true, "license": "MIT" }, "node_modules/content-disposition": { @@ -7365,7 +7309,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "devOptional": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -7408,14 +7351,12 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, "license": "MIT" }, "node_modules/d3-array": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "devOptional": true, "license": "ISC", "dependencies": { "internmap": "1 - 2" @@ -7428,7 +7369,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "devOptional": true, "license": "ISC", "engines": { "node": ">=12" @@ -7438,7 +7378,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.2.tgz", "integrity": "sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==", - "devOptional": true, "license": "ISC", "dependencies": { "delaunator": "5" @@ -7451,7 +7390,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", - "devOptional": true, "license": "ISC", "engines": { "node": ">=12" @@ -7461,7 +7399,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz", "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==", - "devOptional": true, "license": "ISC", "dependencies": { "d3-array": "2.5.0 - 3" @@ -7474,7 +7411,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "devOptional": true, "license": "ISC", "dependencies": { "d3-color": "1 - 3" @@ -7487,7 +7423,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", - "devOptional": true, "license": "ISC", "engines": { "node": ">=12" @@ -7497,7 +7432,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "devOptional": true, "license": "ISC", "dependencies": { "d3-array": "2.10.0 - 3", @@ -7514,7 +7448,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "devOptional": true, "license": "ISC", "dependencies": { "d3-path": "^3.1.0" @@ -7527,7 +7460,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "devOptional": true, "license": "ISC", "dependencies": { "d3-array": "2 - 3" @@ -7540,7 +7472,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "devOptional": true, "license": "ISC", "dependencies": { "d3-time": "1 - 3" @@ -7636,7 +7567,6 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-8.0.2.tgz", "integrity": "sha512-uqbvqLUMrc6p0MO+WBRtTxY55hmyh94WRwI5a++PZe54X+bfVh59FSN7uWCBCW1CCVjzjnrwzfI8zidE2obMMw==", - "devOptional": true, "funding": [ { "type": "ko-fi", @@ -7699,7 +7629,6 @@ "version": "6.1.7", "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", - "devOptional": true, "license": "MIT" }, "node_modules/degenerator": { @@ -7724,7 +7653,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", - "devOptional": true, "license": "ISC", "dependencies": { "robust-predicates": "^3.0.2" @@ -7753,7 +7681,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", - "devOptional": true, "license": "MIT" }, "node_modules/detect-libc": { @@ -7918,7 +7845,6 @@ "version": "3.20.0", "resolved": "https://registry.npmjs.org/effect/-/effect-3.20.0.tgz", "integrity": "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==", - "devOptional": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", @@ -7936,7 +7862,6 @@ "version": "0.11.1", "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.11.1.tgz", "integrity": "sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg==", - "devOptional": true, "license": "EPL-2.0" }, "node_modules/emittery": { @@ -7969,7 +7894,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=14" @@ -8049,7 +7973,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", - "devOptional": true, "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" @@ -8628,14 +8551,12 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.1.tgz", "integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==", - "devOptional": true, "license": "MIT" }, "node_modules/fast-check": { "version": "3.23.2", "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", - "devOptional": true, "funding": [ { "type": "individual", @@ -8658,7 +8579,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "devOptional": true, "funding": [ { "type": "individual", @@ -8675,14 +8595,12 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", - "devOptional": true, "license": "MIT" }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "devOptional": true, "license": "MIT" }, "node_modules/fast-diff": { @@ -8710,7 +8628,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", - "devOptional": true, "license": "MIT", "dependencies": { "fast-decode-uri-component": "^1.0.1" @@ -8743,7 +8660,6 @@ "version": "3.1.7", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", - "devOptional": true, "funding": [ { "type": "github", @@ -8863,7 +8779,6 @@ "version": "9.7.0", "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.7.0.tgz", "integrity": "sha512-f2JHn75x2JlwUwLenZypgczR7YWMb/uO9BvUXtus+JMgkbIkLADd38cI4EiV+OQqrGo1Zlq6V8wnqMJ8e62wUQ==", - "devOptional": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -8914,7 +8829,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "devOptional": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -9022,7 +8936,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", - "devOptional": true, "license": "MIT", "dependencies": { "is-property": "^1.0.2" @@ -9098,7 +9011,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz", "integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==", - "devOptional": true, "license": "MIT" }, "node_modules/get-proto": { @@ -9146,7 +9058,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/giget/-/giget-3.3.1.tgz", "integrity": "sha512-r+mvuDjrjMpsdw46Kmeydb8bdHm7wOKw8wNBtTndkjbPjgAp5oUJUxRE76wZFknxIPokfWvep2qSXK37aXE6zg==", - "devOptional": true, "license": "MIT", "bin": { "giget": "dist/cli.mjs" @@ -9304,21 +9215,18 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "devOptional": true, "license": "ISC" }, "node_modules/grammex": { "version": "3.1.13", "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.13.tgz", "integrity": "sha512-LnPnhOBLEJEVKS8WFDVaA397L9Kq55Q9oSITJiVLHVdhAclfUkWzQv74KhvZHKL2Q09Pb1XdsrOsZ4LfTFFTEg==", - "devOptional": true, "license": "MIT" }, "node_modules/graphmatch": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.1.tgz", "integrity": "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==", - "devOptional": true, "license": "MIT" }, "node_modules/handlebars": { @@ -9630,7 +9538,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "devOptional": true, "license": "ISC", "engines": { "node": ">=12" @@ -9788,7 +9695,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", - "devOptional": true, "license": "MIT" }, "node_modules/is-ssh": { @@ -9847,7 +9753,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "devOptional": true, "license": "ISC" }, "node_modules/issue-parser": { @@ -10528,7 +10433,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", - "devOptional": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -10754,7 +10658,6 @@ "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "devOptional": true, "license": "MIT" }, "node_modules/lodash.camelcase": { @@ -10876,7 +10779,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", - "devOptional": true, "license": "MIT", "engines": { "bun": ">=1.0.0", @@ -11353,7 +11255,6 @@ "version": "3.24.4", "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.24.4.tgz", "integrity": "sha512-A2olluVlj0mvgyIRRISMEzXc51m+21mRtcMVjJyIpt2GG98+XrC9m9HzsqcMsX2LcnfccJvY5NB22g8fENBnOA==", - "devOptional": true, "license": "MIT", "dependencies": { "aws-ssl-profiles": "^1.1.2", @@ -11375,7 +11276,6 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", - "devOptional": true, "license": "MIT", "dependencies": { "lru.min": "^1.1.0" @@ -11609,7 +11509,6 @@ "version": "2.0.12", "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.12.tgz", "integrity": "sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==", - "devOptional": true, "license": "MIT" }, "node_modules/on-exit-leak-free": { @@ -12046,7 +11945,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -12083,7 +11981,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "devOptional": true, "license": "MIT" }, "node_modules/pause": { @@ -12095,7 +11992,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", - "devOptional": true, "license": "MIT" }, "node_modules/picocolors": { @@ -12250,7 +12146,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.3.tgz", "integrity": "sha512-j/lCFdcppV0JxWpCEITdbDltBxPP6cHT+yNJ6Go2OgoSA9518X847X9z0p6LtA4Nc16+eQzCZjRrWanTGvHJ5w==", - "devOptional": true, "license": "MIT", "dependencies": { "confbox": "^0.3.1", @@ -12262,7 +12157,6 @@ "version": "0.3.1", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.3.1.tgz", "integrity": "sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==", - "devOptional": true, "license": "MIT" }, "node_modules/pluralize": { @@ -12279,7 +12173,6 @@ "version": "3.4.7", "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", - "devOptional": true, "license": "Unlicense", "engines": { "node": ">=12" @@ -12401,7 +12294,6 @@ "version": "7.10.0", "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.10.0.tgz", "integrity": "sha512-o0ornyJOWgygVAzGCpr8PdXV8EJLHyVGDDUr/voBQt8Azzw8cYTByzzPGcA/m4tCkPcnJA8raEOv2CslsKhPEw==", - "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -12451,7 +12343,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", - "devOptional": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -12463,7 +12354,6 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "devOptional": true, "license": "ISC" }, "node_modules/protobufjs": { @@ -12709,7 +12599,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.1.0.tgz", "integrity": "sha512-ufjkNVzbRHKcCOmTahZkmVsyc3W+MSk3jY03m+a7tGHkIsdVMG9l10/3HvFbWkkKzY5VFp3pkRsIo/UYgmFL7Q==", - "devOptional": true, "license": "MIT", "dependencies": { "defu": "^6.1.7", @@ -12720,7 +12609,6 @@ "version": "19.2.8", "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", - "devOptional": true, "license": "MIT", "peer": true, "engines": { @@ -12731,7 +12619,6 @@ "version": "19.2.8", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", - "devOptional": true, "license": "MIT", "peer": true, "dependencies": { @@ -12861,7 +12748,6 @@ "version": "2.33.4", "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz", "integrity": "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==", - "devOptional": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/remeda" @@ -12880,7 +12766,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -12956,7 +12841,6 @@ "version": "0.5.0", "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=10" @@ -12966,7 +12850,6 @@ "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "devOptional": true, "license": "MIT", "engines": { "node": ">= 4" @@ -12976,7 +12859,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", - "devOptional": true, "license": "Unlicense" }, "node_modules/router": { @@ -13041,7 +12923,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", - "devOptional": true, "funding": [ { "type": "github", @@ -13079,7 +12960,6 @@ "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "devOptional": true, "license": "MIT", "peer": true }, @@ -13150,7 +13030,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "devOptional": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -13163,7 +13042,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -13245,7 +13123,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "devOptional": true, "license": "ISC", "engines": { "node": ">=14" @@ -13423,7 +13300,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.5.1.tgz", "integrity": "sha512-4toX5E1fQbBrpfXidaHnF0669nkAdETeIPTs2SUjxxD7RRIs9ICG4gtpmfc68JCEKehsdwLFqBu9VlQqZ1P1gg==", - "devOptional": true, "license": "MIT", "engines": { "bun": ">=1.0.0", @@ -13471,7 +13347,6 @@ "version": "3.10.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "devOptional": true, "license": "MIT" }, "node_modules/stdin-discarder": { @@ -14484,7 +14359,6 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.4.2.tgz", "integrity": "sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==", - "devOptional": true, "license": "MIT", "peerDependencies": { "typescript": ">=5" @@ -14737,7 +14611,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "devOptional": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -15026,7 +14899,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz", "integrity": "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==", - "devOptional": true, "license": "MIT", "dependencies": { "grammex": "^3.1.11", diff --git a/package.json b/package.json index 1458b1e..06f3cc9 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "test:debug": "node --experimental-vm-modules --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "npm run build && node test/sqlite.cjs --all", "test:sqlite": "node test/sqlite.cjs", + "test:container": "node test/container-smoke.cjs", "test:durability": "node test/bank-cards-durability.cjs", "test:openapi-contract": "npm run build && node test/sqlite.cjs openapi-contract.e2e-spec.ts", "verify": "npm run lint && npm run typecheck:app && npm run typecheck:test && npm test -- --runInBand && npm run build", @@ -57,6 +58,7 @@ "passport": "0.7.0", "passport-jwt": "4.0.1", "pino-http": "11.0.0", + "prisma": "7.10.0", "reflect-metadata": "0.2.2", "rxjs": "7.8.2" }, @@ -83,7 +85,6 @@ "globals": "17.12.0", "jest": "30.5.1", "prettier": "3.9.6", - "prisma": "7.10.0", "release-it": "21.0.2", "source-map-support": "0.5.21", "supertest": "7.2.2", diff --git a/src/integrations/tbank/tbank.client.ts b/src/integrations/tbank/tbank.client.ts index 9b06f21..4c5b53c 100644 --- a/src/integrations/tbank/tbank.client.ts +++ b/src/integrations/tbank/tbank.client.ts @@ -15,7 +15,7 @@ import { ServiceUnavailableException, } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { existsSync, readFileSync } from 'node:fs'; +import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { StructuredLogger } from '../../common/logging/structured-logger.service'; import { Environment } from '../../config/env.validation'; @@ -172,13 +172,7 @@ export class TbankClient { config: ConfigService, private readonly logger: StructuredLogger, ) { - const sourceProtoRoot = join( - process.cwd(), - 'src/integrations/tbank/proto/contracts', - ); - const protoRoot = existsSync(sourceProtoRoot) - ? sourceProtoRoot - : join(process.cwd(), 'dist/integrations/tbank/proto/contracts'); + const protoRoot = join(__dirname, 'proto/contracts'); const definition = loadSync( [ 'users.proto', diff --git a/test/container-smoke.cjs b/test/container-smoke.cjs new file mode 100644 index 0000000..6d0cb11 --- /dev/null +++ b/test/container-smoke.cjs @@ -0,0 +1,69 @@ +// Real image boundary: catches missing native modules, proto assets and migrations. +const assert = require('node:assert/strict'); +const { execFileSync } = require('node:child_process'); +const { randomUUID } = require('node:crypto'); + +const image = process.argv[2]; +assert.ok(image, 'Pass a built backend image reference'); +const name = `wealthledger-smoke-${randomUUID()}`; +const docker = (...args) => execFileSync('docker', args, { encoding: 'utf8' }).trim(); +const environment = [ + '-e', 'DATABASE_URL=file:/data/wealthledger.db', + '-e', 'FRONTEND_ORIGIN=http://localhost', + '-e', 'JWT_SECRET=disposable-image-smoke-secret-at-least-32-characters', + '-e', 'TOKEN_ENCRYPTION_KEY=MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=', + '-e', 'LOG_LEVEL=error', +]; + +async function ready() { + for (let attempt = 0; attempt < 90; attempt++) { + const state = JSON.parse(docker('inspect', name, '--format', '{{json .State}}')); + assert.equal(state.Running, true, 'Image exited before readiness'); + if (state.Health.Status === 'healthy') return; + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + throw new Error('Image readiness timed out'); +} + +(async () => { + docker('volume', 'create', name); + try { + assert.throws(() => docker('run', '--rm', '--read-only', image), + 'Missing runtime configuration must prevent startup'); + const migrate = () => docker('run', '--rm', '--read-only', '-v', `${name}:/data`, + ...environment, image, 'node', 'node_modules/prisma/build/index.js', 'migrate', 'deploy'); + migrate(); + docker('run', '-d', '--name', name, '--read-only', '-v', `${name}:/data`, ...environment, image); + await ready(); + assert.deepEqual(JSON.parse(docker('inspect', name, '--format', '{{json .HostConfig.PortBindings}}')), {}); + docker('exec', name, 'node', '-e', ` + const assert = require('node:assert/strict'); + const fs = require('node:fs'); + assert.notEqual(process.getuid(), 0); + assert.equal(fs.existsSync('src'), false); + assert.equal(fs.existsSync('.env'), false); + assert.equal(fs.existsSync('node_modules/typescript'), false); + assert.equal(fs.existsSync('node_modules/@nestjs/cli'), false); + fs.accessSync('dist/src/integrations/tbank/proto/contracts/users.proto'); + fetch('http://localhost:3000/api/v1/auth/register', { + method: 'POST', headers: {'content-type':'application/json'}, + body: JSON.stringify({email:'image-smoke@example.test',password:'fictional-image-smoke-password'}) + }).then(async r => {assert.equal(r.status, 200); await r.json()}).catch(e => {console.error(e); process.exitCode=1}); + `); + docker('stop', name); + migrate(); + docker('start', name); + await ready(); + docker('exec', name, 'node', '-e', ` + const assert = require('node:assert/strict'); + const db = require('better-sqlite3')('/data/wealthledger.db'); + assert.equal(db.prepare('SELECT count(*) AS n FROM User').get().n, 1); + assert.equal(db.prepare('SELECT count(*) AS n FROM Session').get().n, 1); + assert.equal(db.pragma('integrity_check', {simple:true}), 'ok'); + db.close(); + `); + console.log('PASS image: fresh/existing migration, non-root read-only runtime, auth, persistence, proto assets'); + } finally { + try { docker('rm', '-f', name); } finally { docker('volume', 'rm', name); } + } +})().catch((error) => { console.error(error); process.exitCode = 1; }); -- 2.54.0 From d7cb7e3e206c995ef2fa6f1ee61ef7e93599d88f Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Wed, 16 Sep 2026 10:48:27 +0300 Subject: [PATCH 5/6] fix: allow reviewed container smoke fixture --- .gitleaksignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitleaksignore b/.gitleaksignore index 733ee42..1751e92 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -15,3 +15,5 @@ dfca11c81f7e6dadce3ca6f961d265c5e3f8ff8e:src/tbank/portfolio.service.spec.ts:gen # Historical CircleCI badge copied verbatim from nestjs/typescript-starter README. # Verified against https://github.com/nestjs/typescript-starter/blob/master/README.md f8dcfb71c1767696a9aa6741ddaf6575416c901e:README.md:generic-api-key:5 +# Container smoke fixture: same deterministic 32-byte test key, never a runtime secret. +1a6be432f24f757728a03415051392069112af1f:test/container-smoke.cjs:generic-api-key:14 -- 2.54.0 From 0557f4ad1f14444900b3dce4f82595590e2557d8 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Wed, 16 Sep 2026 11:35:52 +0300 Subject: [PATCH 6/6] chore(release): 0.2.0 --- CHANGELOG.md | 16 ++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index adb7f38..98265cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## [0.2.0](https://git.personal.ksv741.ru/WealthLedger/backend/compare/v0.1.0...v0.2.0) (2026-09-16) + +### Features + +* add backend container delivery ([1a6be43](https://git.personal.ksv741.ru/WealthLedger/backend/commit/1a6be432f24f757728a03415051392069112af1f)) +* generate OpenAPI without peer repositories ([32c12fb](https://git.personal.ksv741.ru/WealthLedger/backend/commit/32c12fb10d4f739c6af0ce32b8f2ea6e94c42090)) + +### Bug Fixes + +* allow reviewed container smoke fixture ([d7cb7e3](https://git.personal.ksv741.ru/WealthLedger/backend/commit/d7cb7e3e206c995ef2fa6f1ee61ef7e93599d88f)) + +### Documentation + +* refine backend conventions and routing ([9810d47](https://git.personal.ksv741.ru/WealthLedger/backend/commit/9810d47e42d9275cf2520556f517d411058ddcb3)) +* simplify conventions layout ([0167826](https://git.personal.ksv741.ru/WealthLedger/backend/commit/016782657acfd35ff3fd755462007938fac35156)) + ## 0.1.0 (2026-09-14) ### Features diff --git a/package-lock.json b/package-lock.json index 72c6912..1db687f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wealth-ledger-backend", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wealth-ledger-backend", - "version": "0.1.0", + "version": "0.2.0", "license": "UNLICENSED", "dependencies": { "@grpc/grpc-js": "1.14.4", diff --git a/package.json b/package.json index 06f3cc9..9c5aed9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wealth-ledger-backend", - "version": "0.1.0", + "version": "0.2.0", "description": "", "author": "", "private": true, -- 2.54.0